autosnippet 2.12.0 → 2.14.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.
@@ -237,6 +237,80 @@ export async function knowledgeLifecycle(ctx, args) {
237
237
 
238
238
  // ─── 内部辅助 ──────────────────────────────────────────────
239
239
 
240
+ /**
241
+ * 保存开发文档 (autosnippet_save_document)
242
+ *
243
+ * 精简入口:仅需 title + markdown。
244
+ * 自动设置 knowledgeType='dev-document', kind='fact', source='agent'。
245
+ * 不走 RecipeReadiness 检查(文档无需 doClause/trigger)。
246
+ * 支持 autoApprove — 文档直接进入 active 状态。
247
+ */
248
+ export async function saveDocument(ctx, args) {
249
+ if (!args.title || !args.title.trim()) {
250
+ throw new Error('title 必填');
251
+ }
252
+ if (!args.markdown || !args.markdown.trim()) {
253
+ throw new Error('markdown 必填');
254
+ }
255
+
256
+ // 限流
257
+ const blocked = await _checkRateLimit('autosnippet_save_document', args.client_id);
258
+ if (blocked) return blocked;
259
+
260
+ const service = ctx.container.get('knowledgeService');
261
+
262
+ const data = {
263
+ title: args.title.trim(),
264
+ description: args.description || '',
265
+ knowledgeType: 'dev-document',
266
+ kind: 'fact',
267
+ source: args.source || 'agent',
268
+ scope: args.scope || 'project-specific',
269
+ tags: args.tags || [],
270
+ content: {
271
+ markdown: args.markdown,
272
+ pattern: '',
273
+ },
274
+ // 文档不需要 Cursor Delivery 字段
275
+ trigger: '',
276
+ doClause: '',
277
+ dontClause: '',
278
+ whenClause: '',
279
+ topicHint: '',
280
+ coreCode: '',
281
+ // 基础推理
282
+ reasoning: {
283
+ whyStandard: 'Agent development document — preserved for team knowledge',
284
+ sources: ['agent'],
285
+ confidence: 0.8,
286
+ },
287
+ };
288
+
289
+ const entry = await service.create(data, { userId: 'mcp' });
290
+
291
+ // 自动发布(dev-document 不需要人工审核)
292
+ try {
293
+ await service.publish(entry.id, { userId: 'mcp' });
294
+ } catch {
295
+ // 发布失败保持 pending — 非阻塞
296
+ }
297
+
298
+ return envelope({
299
+ success: true,
300
+ data: {
301
+ id: entry.id,
302
+ lifecycle: 'active',
303
+ title: entry.title,
304
+ kind: 'fact',
305
+ knowledgeType: 'dev-document',
306
+ },
307
+ message: `文档「${entry.title}」已保存到知识库。`,
308
+ meta: { tool: 'autosnippet_save_document' },
309
+ });
310
+ }
311
+
312
+ // ─── 内部辅助 ──────────────────────────────────────────────
313
+
240
314
  /**
241
315
  * V3 wire format → RecipeReadinessChecker 兼容格式
242
316
  */
@@ -23,6 +23,7 @@ export const TOOL_GATEWAY_MAP = {
23
23
  autosnippet_submit_knowledge: { action: 'knowledge:create', resource: 'knowledge' },
24
24
  autosnippet_submit_knowledge_batch: { action: 'knowledge:create', resource: 'knowledge' },
25
25
  autosnippet_knowledge_lifecycle: { action: 'knowledge:update', resource: 'knowledge' },
26
+ autosnippet_save_document: { action: 'knowledge:create', resource: 'knowledge' },
26
27
  };
27
28
 
28
29
  export const TOOLS = [
@@ -751,4 +752,26 @@ export const TOOLS = [
751
752
  required: ['id', 'action'],
752
753
  },
753
754
  },
755
+ // 39. 保存开发文档
756
+ {
757
+ name: 'autosnippet_save_document',
758
+ description:
759
+ '保存开发文档到知识库(架构设计、排查报告、决策记录、调研笔记等)。\n' +
760
+ '仅需 title + markdown,无需填写 kind/doClause/trigger 等 Cursor Delivery 字段。\n' +
761
+ '文档自动以 dev-document 类型存储,不参与 Cursor Rules 压缩,保持原文完整性。\n' +
762
+ '交付路径: Channel D → .cursor/skills/autosnippet-devdocs/references/*.md。\n' +
763
+ 'Agent 可通过 autosnippet_search 全文检索已保存的文档。',
764
+ inputSchema: {
765
+ type: 'object',
766
+ properties: {
767
+ title: { type: 'string', description: '文档标题(中英文皆可)' },
768
+ markdown: { type: 'string', description: '文档 Markdown 全文' },
769
+ description: { type: 'string', description: '一句话摘要(可选)' },
770
+ tags: { type: 'array', items: { type: 'string' }, description: '标签列表,如 ["adr", "debug-report", "design-doc", "research"]' },
771
+ scope: { type: 'string', enum: ['universal', 'project-specific'], default: 'project-specific', description: '适用范围' },
772
+ source: { type: 'string', description: '来源标识(默认 agent)' },
773
+ },
774
+ required: ['title', 'markdown'],
775
+ },
776
+ },
754
777
  ];
@@ -291,4 +291,31 @@ router.post('/import-from-recipe', asyncHandler(async (req, res) => {
291
291
  });
292
292
  }));
293
293
 
294
+ /**
295
+ * GET /api/v1/rules/compliance
296
+ * 生成全项目合规报告
297
+ * Query params:
298
+ * - path: 扫描根目录(默认 projectRoot)
299
+ * - maxErrors: Quality Gate 最大 error 数(默认 0)
300
+ * - maxWarnings: Quality Gate 最大 warning 数(默认 20)
301
+ * - minScore: Quality Gate 最低分(默认 70)
302
+ * - maxFiles: 最大扫描文件数(默认 500)
303
+ */
304
+ router.get('/compliance', asyncHandler(async (req, res) => {
305
+ const container = getServiceContainer();
306
+ const reporter = container.get('complianceReporter');
307
+ const projectRoot = req.query.path || process.env.ASD_PROJECT_DIR || process.cwd();
308
+
309
+ const report = await reporter.generate(projectRoot, {
310
+ qualityGate: {
311
+ maxErrors: parseInt(req.query.maxErrors) || 0,
312
+ maxWarnings: parseInt(req.query.maxWarnings) || 20,
313
+ minScore: parseInt(req.query.minScore) || 70,
314
+ },
315
+ maxFiles: parseInt(req.query.maxFiles) || 500,
316
+ });
317
+
318
+ res.json({ success: true, data: report });
319
+ }));
320
+
294
321
  export default router;
@@ -34,6 +34,8 @@ import { SnippetInstaller } from '../service/snippet/SnippetInstaller.js';
34
34
  import { ExclusionManager } from '../service/guard/ExclusionManager.js';
35
35
  import { RuleLearner } from '../service/guard/RuleLearner.js';
36
36
  import { ViolationsStore } from '../service/guard/ViolationsStore.js';
37
+ import { ComplianceReporter } from '../service/guard/ComplianceReporter.js';
38
+ import { GuardFeedbackLoop } from '../service/guard/GuardFeedbackLoop.js';
37
39
 
38
40
  // ─── P1: Token Usage Tracking ─────────────────────────
39
41
  import { TokenUsageStore } from '../repository/token/TokenUsageStore.js';
@@ -459,6 +461,33 @@ export class ServiceContainer {
459
461
  return this.singletons.violationsStore;
460
462
  });
461
463
 
464
+ // Guard: ComplianceReporter
465
+ this.register('complianceReporter', () => {
466
+ if (!this.singletons.complianceReporter) {
467
+ const config = this.singletons._config || {};
468
+ this.singletons.complianceReporter = new ComplianceReporter(
469
+ this.get('guardCheckEngine'),
470
+ this.get('violationsStore'),
471
+ this.get('ruleLearner'),
472
+ this.get('exclusionManager'),
473
+ config.qualityGate || {},
474
+ );
475
+ }
476
+ return this.singletons.complianceReporter;
477
+ });
478
+
479
+ // Guard: GuardFeedbackLoop
480
+ this.register('guardFeedbackLoop', () => {
481
+ if (!this.singletons.guardFeedbackLoop) {
482
+ this.singletons.guardFeedbackLoop = new GuardFeedbackLoop(
483
+ this.get('violationsStore'),
484
+ this.get('feedbackCollector'),
485
+ { guardCheckEngine: this.get('guardCheckEngine') },
486
+ );
487
+ }
488
+ return this.singletons.guardFeedbackLoop;
489
+ });
490
+
462
491
  // Token Usage: 持久化 AI token 消耗
463
492
  this.register('tokenUsageStore', () => {
464
493
  if (!this.singletons.tokenUsageStore) {
@@ -211,9 +211,20 @@ function _auditSingleFile(engine, fullPath, code, detectLanguage, scope = 'file'
211
211
  console.log(` 🛡️ ${errors.length} errors, ${warnings.length} warnings`);
212
212
  for (const v of errors) {
213
213
  console.log(` ❌ L${v.line} [${v.ruleId}] ${v.message}`);
214
+ if (v.fixSuggestion) console.log(` 🔧 修复建议: ${v.fixSuggestion}`);
214
215
  }
215
216
  for (const v of warnings.slice(0, 5)) {
216
217
  console.log(` ⚠️ L${v.line} [${v.ruleId}] ${v.message}`);
218
+ if (v.fixSuggestion) console.log(` 🔧 修复建议: ${v.fixSuggestion}`);
217
219
  }
218
220
  }
221
+
222
+ // Guard ↔ Recipe 闭环:检测修复并自动确认使用(fire-and-forget)
223
+ import('../../../injection/ServiceContainer.js').then(({ ServiceContainer }) => {
224
+ try {
225
+ const container = ServiceContainer.getInstance();
226
+ const feedbackLoop = container.get('guardFeedbackLoop');
227
+ feedbackLoop.processFixDetection({ violations }, fullPath);
228
+ } catch { /* guardFeedbackLoop not available */ }
229
+ }).catch(() => { /* ignored */ });
219
230
  }
@@ -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,
@@ -16,6 +16,7 @@ import { RulesGenerator } from './RulesGenerator.js';
16
16
  import { SkillsSyncer } from './SkillsSyncer.js';
17
17
  import { estimateTokens, BUDGET } from './TokenBudget.js';
18
18
  import path from 'node:path';
19
+ import fs from 'node:fs';
19
20
 
20
21
  export class CursorDeliveryPipeline {
21
22
  /**
@@ -48,6 +49,7 @@ export class CursorDeliveryPipeline {
48
49
  channelA: { rulesCount: 0, tokensUsed: 0 },
49
50
  channelB: { topicCount: 0, patternsCount: 0, totalTokens: 0 },
50
51
  channelC: { synced: 0, skipped: 0, errors: 0 },
52
+ channelD: { documentsCount: 0, filesWritten: 0 },
51
53
  totalTokensUsed: 0,
52
54
  duration: 0,
53
55
  };
@@ -57,9 +59,9 @@ export class CursorDeliveryPipeline {
57
59
  const entries = await this._loadEntries();
58
60
  this.logger.info?.(`[CursorDelivery] Loaded ${entries.length} knowledge entries`);
59
61
 
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`);
62
+ // 2. 分类:rules vs patterns vs facts vs documents
63
+ const { rules, patterns, documents } = this._classify(entries);
64
+ this.logger.info?.(`[CursorDelivery] Classified: ${rules.length} rules, ${patterns.length} patterns, ${documents.length} documents`);
63
65
 
64
66
  // 3. 清理旧的动态生成文件
65
67
  this.rulesGenerator.cleanDynamicFiles();
@@ -76,6 +78,10 @@ export class CursorDeliveryPipeline {
76
78
  const channelC = await this._generateChannelC();
77
79
  stats.channelC = channelC;
78
80
 
81
+ // ── Channel D: Dev Documents → references ──
82
+ const channelD = this._generateChannelD(documents);
83
+ stats.channelD = channelD;
84
+
79
85
  // 统计
80
86
  stats.totalTokensUsed = channelA.tokensUsed + channelB.totalTokens;
81
87
  stats.duration = Date.now() - startTime;
@@ -83,9 +89,10 @@ export class CursorDeliveryPipeline {
83
89
  this.logger.info?.(`[CursorDelivery] Done in ${stats.duration}ms — ` +
84
90
  `A: ${channelA.rulesCount} rules (${channelA.tokensUsed} tokens), ` +
85
91
  `B: ${channelB.topicCount} topics (${channelB.totalTokens} tokens), ` +
86
- `C: ${channelC.synced} skills synced`);
92
+ `C: ${channelC.synced} skills synced, ` +
93
+ `D: ${channelD.documentsCount} documents`);
87
94
 
88
- return { channelA, channelB, channelC, stats };
95
+ return { channelA, channelB, channelC, channelD, stats };
89
96
  } catch (error) {
90
97
  this.logger.error?.(`[CursorDelivery] Error: ${error.message}`);
91
98
  throw error;
@@ -146,16 +153,23 @@ export class CursorDeliveryPipeline {
146
153
 
147
154
  /**
148
155
  * 按 kind 分类知识条目
156
+ * dev-document 类型单独分流,不进入 Channel A/B 压缩
149
157
  * @private
150
158
  */
151
159
  _classify(entries) {
152
- const rules = [], patterns = [], facts = [];
160
+ const rules = [], patterns = [], facts = [], documents = [];
153
161
  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
162
+ if (entry.knowledgeType === 'dev-document') {
163
+ documents.push(entry);
164
+ } else if (entry.kind === 'rule') {
165
+ rules.push(entry);
166
+ } else if (entry.kind === 'fact') {
167
+ facts.push(entry);
168
+ } else {
169
+ patterns.push(entry); // 无 kind 或 kind='pattern' → pattern
170
+ }
157
171
  }
158
- return { rules, patterns, facts };
172
+ return { rules, patterns, facts, documents };
159
173
  }
160
174
 
161
175
  /**
@@ -267,6 +281,98 @@ export class CursorDeliveryPipeline {
267
281
  }
268
282
  }
269
283
 
284
+ /**
285
+ * Channel D — Dev Documents 生成
286
+ * 将 knowledgeType='dev-document' 的条目以原始 MD 写入
287
+ * .cursor/skills/autosnippet-devdocs/references/ 目录
288
+ * @private
289
+ */
290
+ _generateChannelD(documents) {
291
+ const result = { documentsCount: 0, filesWritten: 0, filePaths: [] };
292
+ if (!documents || documents.length === 0) {
293
+ return result;
294
+ }
295
+
296
+ const devdocsDir = path.join(this.projectRoot, '.cursor', 'skills', 'autosnippet-devdocs');
297
+ const refsDir = path.join(devdocsDir, 'references');
298
+ fs.mkdirSync(refsDir, { recursive: true });
299
+
300
+ // 生成 SKILL.md(索引页)
301
+ const skillLines = [
302
+ '---',
303
+ 'name: autosnippet-devdocs',
304
+ `description: "Development documents and knowledge artifacts for ${this.projectName}. Use when looking up architecture decisions, debug reports, design docs, or analysis notes."`,
305
+ '---',
306
+ '',
307
+ `# Dev Documents — ${this.projectName}`,
308
+ '',
309
+ 'Use this skill when:',
310
+ '- Looking up architecture decisions or design rationale',
311
+ '- Reviewing debug reports or performance analysis',
312
+ '- Finding previous research or investigation notes',
313
+ '- Understanding project-specific decisions and trade-offs',
314
+ '',
315
+ '## Document Index',
316
+ '',
317
+ '| Title | Tags | Updated |',
318
+ '|-------|------|---------|',
319
+ ];
320
+
321
+ for (const doc of documents) {
322
+ const tags = (doc.tags || []).join(', ') || '-';
323
+ const updated = doc.updatedAt
324
+ ? new Date(doc.updatedAt * 1000).toISOString().split('T')[0]
325
+ : '-';
326
+ const slug = this._slugify(doc.title || doc.id);
327
+ skillLines.push(`| [${doc.title}](references/${slug}.md) | ${tags} | ${updated} |`);
328
+
329
+ // 写入单个文档 MD
330
+ const markdown = doc.content?.markdown || doc.description || '';
331
+ const docContent = [
332
+ `# ${doc.title || 'Untitled'}`,
333
+ '',
334
+ doc.description ? `> ${doc.description}` : '',
335
+ '',
336
+ `**Tags:** ${tags} `,
337
+ `**Scope:** ${doc.scope || 'universal'} `,
338
+ `**Created:** ${doc.createdAt ? new Date(doc.createdAt * 1000).toISOString().split('T')[0] : '-'}`,
339
+ '',
340
+ '---',
341
+ '',
342
+ markdown,
343
+ ].filter(Boolean).join('\n');
344
+
345
+ const docPath = path.join(refsDir, `${slug}.md`);
346
+ fs.writeFileSync(docPath, docContent, 'utf8');
347
+ result.filePaths.push(docPath);
348
+ result.filesWritten++;
349
+ }
350
+
351
+ skillLines.push('');
352
+ skillLines.push('## Deeper Knowledge');
353
+ skillLines.push('');
354
+ skillLines.push('For full-text search across all documents:');
355
+ skillLines.push('- `autosnippet_search("your query")`');
356
+
357
+ fs.writeFileSync(path.join(devdocsDir, 'SKILL.md'), skillLines.join('\n') + '\n', 'utf8');
358
+ result.documentsCount = documents.length;
359
+
360
+ this.logger.info?.(`[CursorDelivery] Channel D: ${result.documentsCount} documents → ${refsDir}`);
361
+ return result;
362
+ }
363
+
364
+ /**
365
+ * 文件名安全 slug 化
366
+ * @private
367
+ */
368
+ _slugify(text) {
369
+ return text
370
+ .toLowerCase()
371
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
372
+ .replace(/^-+|-+$/g, '')
373
+ .substring(0, 80) || 'untitled';
374
+ }
375
+
270
376
  /**
271
377
  * 从项目路径推断项目名称
272
378
  * @private