autosnippet 3.0.11 → 3.1.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.
Files changed (120) hide show
  1. package/bin/api-server.js +2 -0
  2. package/bin/cli.js +84 -16
  3. package/config/default.json +10 -1
  4. package/dashboard/dist/assets/{index-I2ySoCmF.js → index-Bnm26ulL.js} +47 -47
  5. package/dashboard/dist/index.html +1 -1
  6. package/lib/bootstrap.js +4 -4
  7. package/lib/cli/SetupService.js +116 -29
  8. package/lib/cli/UpgradeService.js +16 -6
  9. package/lib/core/AstAnalyzer.js +1 -1
  10. package/lib/core/ast/ensure-grammars.js +1 -1
  11. package/lib/core/ast/index.js +62 -11
  12. package/lib/core/ast/lang-dart.js +27 -21
  13. package/lib/core/ast/lang-go.js +6 -20
  14. package/lib/core/ast/lang-rust.js +53 -28
  15. package/lib/core/ast/parser-init.js +9 -5
  16. package/lib/core/discovery/DartDiscoverer.js +4 -10
  17. package/lib/core/discovery/GenericDiscoverer.js +4 -28
  18. package/lib/core/discovery/GoDiscoverer.js +45 -25
  19. package/lib/core/discovery/NodeDiscoverer.js +1 -3
  20. package/lib/core/discovery/PythonDiscoverer.js +7 -1
  21. package/lib/core/discovery/RustDiscoverer.js +111 -38
  22. package/lib/core/discovery/index.js +2 -2
  23. package/lib/core/enhancement/django-enhancement.js +10 -4
  24. package/lib/core/enhancement/fastapi-enhancement.js +16 -9
  25. package/lib/core/enhancement/go-grpc-enhancement.js +2 -1
  26. package/lib/core/enhancement/go-web-enhancement.js +3 -6
  27. package/lib/core/enhancement/ml-enhancement.js +6 -3
  28. package/lib/core/enhancement/nextjs-enhancement.js +17 -7
  29. package/lib/core/enhancement/node-server-enhancement.js +4 -2
  30. package/lib/core/enhancement/react-enhancement.js +6 -3
  31. package/lib/core/enhancement/rust-tokio-enhancement.js +6 -2
  32. package/lib/core/enhancement/rust-web-enhancement.js +13 -7
  33. package/lib/core/enhancement/vue-enhancement.js +10 -5
  34. package/lib/external/ai/AiFactory.js +3 -1
  35. package/lib/external/ai/AiProvider.js +3 -1
  36. package/lib/external/mcp/McpServer.js +2 -0
  37. package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +245 -0
  38. package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +86 -0
  39. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +275 -0
  40. package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +629 -0
  41. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +131 -348
  42. package/lib/external/mcp/handlers/bootstrap/refine.js +364 -0
  43. package/lib/external/mcp/handlers/bootstrap.js +7 -597
  44. package/lib/external/mcp/handlers/browse.js +123 -9
  45. package/lib/external/mcp/handlers/guard.js +29 -6
  46. package/lib/external/mcp/handlers/search.js +56 -24
  47. package/lib/external/mcp/handlers/skill.js +6 -2
  48. package/lib/http/HttpServer.js +1 -1
  49. package/lib/http/routes/candidates.js +3 -1
  50. package/lib/http/routes/extract.js +4 -5
  51. package/lib/http/routes/guardRules.js +9 -17
  52. package/lib/http/routes/modules.js +9 -3
  53. package/lib/http/routes/skills.js +54 -6
  54. package/lib/http/routes/violations.js +4 -3
  55. package/lib/infrastructure/external/ClipboardManager.js +24 -7
  56. package/lib/infrastructure/external/NativeUi.js +3 -1
  57. package/lib/infrastructure/external/OpenBrowser.js +1 -0
  58. package/lib/infrastructure/external/XcodeAutomation.js +5 -5
  59. package/lib/infrastructure/vector/IndexingPipeline.js +14 -5
  60. package/lib/injection/ServiceContainer.js +45 -13
  61. package/lib/platform/ios/index.js +20 -25
  62. package/lib/platform/ios/routes/spm.js +6 -3
  63. package/lib/platform/ios/snippet/PlaceholderConverter.js +6 -2
  64. package/lib/platform/ios/snippet/XcodeCodec.js +4 -2
  65. package/lib/platform/ios/spm/SpmDiscoverer.js +1 -1
  66. package/lib/platform/ios/spm/SpmService.js +3 -1
  67. package/lib/platform/ios/xcode/XcodeImportResolver.js +434 -0
  68. package/lib/platform/ios/xcode/XcodeIntegration.js +43 -664
  69. package/lib/platform/ios/xcode/XcodeWriteUtils.js +225 -0
  70. package/lib/service/automation/FileWatcher.js +1 -3
  71. package/lib/service/automation/handlers/CreateHandler.js +3 -5
  72. package/lib/service/automation/handlers/GuardHandler.js +11 -32
  73. package/lib/service/automation/handlers/SearchHandler.js +9 -9
  74. package/lib/service/chat/CandidateGuardrail.js +11 -6
  75. package/lib/service/chat/ChatAgent.js +51 -421
  76. package/lib/service/chat/ChatAgentPrompts.js +149 -0
  77. package/lib/service/chat/ChatAgentTasks.js +297 -0
  78. package/lib/service/chat/HandoffProtocol.js +5 -2
  79. package/lib/service/chat/tools/_shared.js +61 -0
  80. package/lib/service/chat/tools/ai-analysis.js +284 -0
  81. package/lib/service/chat/tools/ast-graph.js +681 -0
  82. package/lib/service/chat/tools/composite.js +497 -0
  83. package/lib/service/chat/tools/guard.js +265 -0
  84. package/lib/service/chat/tools/index.js +239 -0
  85. package/lib/service/chat/tools/infrastructure.js +227 -0
  86. package/lib/service/chat/tools/knowledge-graph.js +234 -0
  87. package/lib/service/chat/tools/lifecycle.js +486 -0
  88. package/lib/service/chat/tools/project-access.js +919 -0
  89. package/lib/service/chat/tools/query.js +264 -0
  90. package/lib/service/chat/tools.js +13 -3994
  91. package/lib/service/cursor/AgentInstructionsGenerator.js +413 -0
  92. package/lib/service/cursor/CursorDeliveryPipeline.js +71 -11
  93. package/lib/service/cursor/FileProtection.js +116 -0
  94. package/lib/service/cursor/KnowledgeCompressor.js +70 -11
  95. package/lib/service/cursor/SkillsSyncer.js +5 -3
  96. package/lib/service/cursor/TopicClassifier.js +19 -3
  97. package/lib/service/guard/ComplianceReporter.js +5 -2
  98. package/lib/service/guard/ExclusionManager.js +26 -2
  99. package/lib/service/guard/GuardCheckEngine.js +83 -388
  100. package/lib/service/guard/GuardCodeChecks.js +391 -0
  101. package/lib/service/guard/GuardCrossFileChecks.js +326 -0
  102. package/lib/service/guard/GuardPatternUtils.js +187 -0
  103. package/lib/service/guard/GuardService.js +80 -38
  104. package/lib/service/module/ModuleService.js +181 -56
  105. package/lib/service/recipe/RecipeCandidateValidator.js +11 -8
  106. package/lib/service/search/SearchEngine.js +10 -2
  107. package/lib/service/snippet/SnippetFactory.js +3 -3
  108. package/lib/service/snippet/SnippetInstaller.js +35 -11
  109. package/lib/service/snippet/codecs/VSCodeCodec.js +2 -2
  110. package/lib/service/wiki/WikiGenerator.js +247 -1535
  111. package/lib/service/wiki/WikiRenderers.js +1903 -0
  112. package/lib/service/wiki/WikiUtils.js +1044 -0
  113. package/lib/shared/LanguageService.js +359 -2
  114. package/lib/shared/PathGuard.js +0 -8
  115. package/package.json +3 -9
  116. package/scripts/bench-real-projects.mjs +29 -29
  117. package/scripts/generate-recipe-drafts.js +17 -27
  118. package/scripts/init-snippets.js +43 -24
  119. package/scripts/install-vscode-copilot.js +3 -19
  120. package/scripts/setup-mcp-config.js +0 -4
@@ -6,26 +6,69 @@
6
6
  * 语言特有操作(如 SPM 依赖管理)由对应的 Discoverer / Service 直接暴露,不经此类代理。
7
7
  */
8
8
 
9
- import { basename as _pathBasename, relative, join as _pathJoin, extname as _pathExtname, isAbsolute as _pathIsAbsolute } from 'node:path';
10
- import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
11
- import Logger from '../../infrastructure/logging/Logger.js';
9
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
10
+ import {
11
+ basename as _pathBasename,
12
+ extname as _pathExtname,
13
+ isAbsolute as _pathIsAbsolute,
14
+ join as _pathJoin,
15
+ relative,
16
+ } from 'node:path';
12
17
  import { getDiscovererRegistry } from '../../core/discovery/index.js';
13
18
  import { inferLang } from '../../external/mcp/handlers/LanguageExtensions.js';
19
+ import Logger from '../../infrastructure/logging/Logger.js';
14
20
 
15
21
  /** 全局排除目录 */
16
22
  const SCAN_EXCLUDE_DIRS = new Set([
17
- 'node_modules', '.git', 'dist', 'build', '.next', 'Pods',
18
- 'Carthage', '.build', 'DerivedData', 'vendor', '__pycache__',
19
- '.venv', 'venv', 'target', '.gradle', '.idea', 'out', 'coverage',
20
- '.cache', '.tox', '.mypy_cache', '.pytest_cache', 'AutoSnippet',
23
+ 'node_modules',
24
+ '.git',
25
+ 'dist',
26
+ 'build',
27
+ '.next',
28
+ 'Pods',
29
+ 'Carthage',
30
+ '.build',
31
+ 'DerivedData',
32
+ 'vendor',
33
+ '__pycache__',
34
+ '.venv',
35
+ 'venv',
36
+ 'target',
37
+ '.gradle',
38
+ '.idea',
39
+ 'out',
40
+ 'coverage',
41
+ '.cache',
42
+ '.tox',
43
+ '.mypy_cache',
44
+ '.pytest_cache',
45
+ 'AutoSnippet',
21
46
  ]);
22
47
 
23
48
  /** 源码文件扩展名 */
24
49
  const SOURCE_CODE_EXTS = new Set([
25
- '.swift', '.m', '.mm', '.h',
26
- '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs',
27
- '.py', '.java', '.kt', '.kts', '.go', '.rs', '.rb',
28
- '.vue', '.svelte', '.c', '.cpp', '.cs',
50
+ '.swift',
51
+ '.m',
52
+ '.mm',
53
+ '.h',
54
+ '.js',
55
+ '.ts',
56
+ '.tsx',
57
+ '.jsx',
58
+ '.mjs',
59
+ '.cjs',
60
+ '.py',
61
+ '.java',
62
+ '.kt',
63
+ '.kts',
64
+ '.go',
65
+ '.rs',
66
+ '.rb',
67
+ '.vue',
68
+ '.svelte',
69
+ '.c',
70
+ '.cpp',
71
+ '.cs',
29
72
  ]);
30
73
 
31
74
  export class ModuleService {
@@ -82,7 +125,9 @@ export class ModuleService {
82
125
  * 自动检测项目类型并加载所有匹配的 Discoverer
83
126
  */
84
127
  async load() {
85
- if (this.#loaded) return;
128
+ if (this.#loaded) {
129
+ return;
130
+ }
86
131
 
87
132
  const matches = await this.#registry.detectAll(this.#projectRoot);
88
133
  this.#activeDiscoverers = [];
@@ -143,12 +188,16 @@ export class ModuleService {
143
188
 
144
189
  // 第一遍:加载非 generic 的 Discoverer(真实项目结构识别器)
145
190
  for (const { discoverer } of this.#activeDiscoverers) {
146
- if (discoverer.id === 'generic') continue;
191
+ if (discoverer.id === 'generic') {
192
+ continue;
193
+ }
147
194
  try {
148
195
  const targets = await discoverer.listTargets();
149
196
  for (const t of targets) {
150
197
  const key = `${discoverer.id}::${t.name}`;
151
- if (seenNames.has(key)) continue;
198
+ if (seenNames.has(key)) {
199
+ continue;
200
+ }
152
201
  seenNames.add(key);
153
202
  allTargets.push(this.#normalizeTarget(t, discoverer));
154
203
  hasRealDiscovererTargets = true;
@@ -163,12 +212,16 @@ export class ModuleService {
163
212
  // 第二遍:仅当没有真实 Discoverer 产出 target 时,才加载 GenericDiscoverer 的结果(兜底)
164
213
  if (!hasRealDiscovererTargets) {
165
214
  for (const { discoverer } of this.#activeDiscoverers) {
166
- if (discoverer.id !== 'generic') continue;
215
+ if (discoverer.id !== 'generic') {
216
+ continue;
217
+ }
167
218
  try {
168
219
  const targets = await discoverer.listTargets();
169
220
  for (const t of targets) {
170
221
  const key = `${discoverer.id}::${t.name}`;
171
- if (seenNames.has(key)) continue;
222
+ if (seenNames.has(key)) {
223
+ continue;
224
+ }
172
225
  seenNames.add(key);
173
226
  allTargets.push(this.#normalizeTarget(t, discoverer));
174
227
  }
@@ -235,14 +288,14 @@ export class ModuleService {
235
288
  if (targets.some((t) => t.name === targetObj.name)) {
236
289
  return discoverer.getTargetFiles(targetObj);
237
290
  }
238
- } catch {
239
- continue;
240
- }
291
+ } catch {}
241
292
  }
242
293
 
243
294
  // 兜底:如果 target 有 path 属性且目录存在,直接收集
244
295
  if (targetObj.path && existsSync(targetObj.path)) {
245
- this.#logger.info(`[ModuleService] getTargetFiles fallback: collecting from ${targetObj.path}`);
296
+ this.#logger.info(
297
+ `[ModuleService] getTargetFiles fallback: collecting from ${targetObj.path}`
298
+ );
246
299
  return this.#collectFolderFiles(targetObj.path);
247
300
  }
248
301
 
@@ -313,6 +366,7 @@ export class ModuleService {
313
366
 
314
367
  return {
315
368
  projectRoot: this.#projectRoot,
369
+ projectName: _pathBasename(this.#projectRoot) || '',
316
370
  primaryLanguage: primaryDiscoverer
317
371
  ? this.#discovererToLanguage(primaryDiscoverer.id)
318
372
  : 'unknown',
@@ -458,7 +512,9 @@ export class ModuleService {
458
512
  const fileList = await this.getTargetFiles(t);
459
513
  for (const f of fileList) {
460
514
  const fp = typeof f === 'string' ? f : f.path;
461
- if (seenPaths.has(fp)) continue;
515
+ if (seenPaths.has(fp)) {
516
+ continue;
517
+ }
462
518
  seenPaths.add(fp);
463
519
  try {
464
520
  const content = readFileSync(fp, 'utf8');
@@ -469,19 +525,27 @@ export class ModuleService {
469
525
  content,
470
526
  targetName: t.name,
471
527
  });
472
- } catch { /* unreadable */ }
473
- if (allFiles.length >= MAX_FILES) break;
528
+ } catch {
529
+ /* unreadable */
530
+ }
531
+ if (allFiles.length >= MAX_FILES) {
532
+ break;
533
+ }
474
534
  }
475
535
  } catch (e) {
476
536
  this.#logger.warn(`[ModuleService] scanProject: skipping module ${t.name}: ${e.message}`);
477
537
  }
478
- if (allFiles.length >= MAX_FILES) break;
538
+ if (allFiles.length >= MAX_FILES) {
539
+ break;
540
+ }
479
541
  }
480
542
  }
481
543
 
482
544
  // 如果没有 target 收集到文件,回退到目录扫描
483
545
  if (allFiles.length === 0) {
484
- this.#logger.info('[ModuleService] scanProject: No module targets, falling back to directory scan');
546
+ this.#logger.info(
547
+ '[ModuleService] scanProject: No module targets, falling back to directory scan'
548
+ );
485
549
  this.#walkProjectForFiles(allFiles, seenPaths, MAX_FILES);
486
550
  }
487
551
 
@@ -610,9 +674,7 @@ export class ModuleService {
610
674
  * @returns {Promise<Array<{ name: string, path: string, depth: number, language: string, sourceFileCount: number, hasSourceFiles: boolean }>>}
611
675
  */
612
676
  async browseDirectories(basePath = '', maxDepth = 2) {
613
- const root = basePath
614
- ? _pathJoin(this.#projectRoot, basePath)
615
- : this.#projectRoot;
677
+ const root = basePath ? _pathJoin(this.#projectRoot, basePath) : this.#projectRoot;
616
678
 
617
679
  if (!existsSync(root)) {
618
680
  return [];
@@ -726,7 +788,9 @@ export class ModuleService {
726
788
  createProvider,
727
789
  } = this.#aiFactory;
728
790
  let ai = await getProviderWithFallback();
729
- if (!ai) return [];
791
+ if (!ai) {
792
+ return [];
793
+ }
730
794
 
731
795
  // 加载语言参考 Skill
732
796
  let extractOpts = {};
@@ -740,7 +804,9 @@ export class ModuleService {
740
804
  extractOpts = { skillReference: skillCtx.languageSkill.substring(0, 2000) };
741
805
  }
742
806
  }
743
- } catch { /* Skills not available */ }
807
+ } catch {
808
+ /* Skills not available */
809
+ }
744
810
 
745
811
  try {
746
812
  return await ai.extractRecipes(targetName, files, extractOpts);
@@ -797,13 +863,21 @@ export class ModuleService {
797
863
  * 目录遍历 — 浏览子目录结构
798
864
  */
799
865
  #walkDirsForBrowse(dir, dirs, depth, maxDepth) {
800
- if (depth >= maxDepth) return;
866
+ if (depth >= maxDepth) {
867
+ return;
868
+ }
801
869
  try {
802
870
  const entries = readdirSync(dir, { withFileTypes: true });
803
871
  for (const entry of entries) {
804
- if (!entry.isDirectory()) continue;
805
- if (entry.name.startsWith('.')) continue;
806
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
872
+ if (!entry.isDirectory()) {
873
+ continue;
874
+ }
875
+ if (entry.name.startsWith('.')) {
876
+ continue;
877
+ }
878
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) {
879
+ continue;
880
+ }
807
881
 
808
882
  const fullPath = _pathJoin(dir, entry.name);
809
883
  const relativePath = relative(this.#projectRoot, fullPath);
@@ -825,14 +899,18 @@ export class ModuleService {
825
899
 
826
900
  this.#walkDirsForBrowse(fullPath, dirs, depth + 1, maxDepth);
827
901
  }
828
- } catch { /* skip */ }
902
+ } catch {
903
+ /* skip */
904
+ }
829
905
  }
830
906
 
831
907
  /**
832
908
  * 递归统计目录下源码文件数(限深度 + 上限 999 防止超大目录卡顿)
833
909
  */
834
910
  #countSourceFilesDeep(dir, maxDepth, depth = 0) {
835
- if (depth >= maxDepth) return 0;
911
+ if (depth >= maxDepth) {
912
+ return 0;
913
+ }
836
914
  let count = 0;
837
915
  try {
838
916
  const entries = readdirSync(dir, { withFileTypes: true });
@@ -842,9 +920,13 @@ export class ModuleService {
842
920
  } else if (e.isDirectory() && !e.name.startsWith('.') && !SCAN_EXCLUDE_DIRS.has(e.name)) {
843
921
  count += this.#countSourceFilesDeep(_pathJoin(dir, e.name), maxDepth, depth + 1);
844
922
  }
845
- if (count >= 999) return count;
923
+ if (count >= 999) {
924
+ return count;
925
+ }
846
926
  }
847
- } catch { /* skip */ }
927
+ } catch {
928
+ /* skip */
929
+ }
848
930
  return count;
849
931
  }
850
932
 
@@ -861,12 +943,18 @@ export class ModuleService {
861
943
  * 递归收集源码文件
862
944
  */
863
945
  #walkCollectSourceFiles(dir, rootDir, files, depth, maxDepth) {
864
- if (depth > maxDepth || files.length > 500) return;
946
+ if (depth > maxDepth || files.length > 500) {
947
+ return;
948
+ }
865
949
  try {
866
950
  const entries = readdirSync(dir, { withFileTypes: true });
867
951
  for (const entry of entries) {
868
- if (entry.name.startsWith('.')) continue;
869
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
952
+ if (entry.name.startsWith('.')) {
953
+ continue;
954
+ }
955
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) {
956
+ continue;
957
+ }
870
958
 
871
959
  const fullPath = _pathJoin(dir, entry.name);
872
960
  if (entry.isDirectory()) {
@@ -883,7 +971,9 @@ export class ModuleService {
883
971
  }
884
972
  }
885
973
  }
886
- } catch { /* skip */ }
974
+ } catch {
975
+ /* skip */
976
+ }
887
977
  }
888
978
 
889
979
  /**
@@ -894,15 +984,21 @@ export class ModuleService {
894
984
  try {
895
985
  const entries = readdirSync(dirPath, { withFileTypes: true });
896
986
  for (const entry of entries) {
897
- if (!entry.isFile()) continue;
987
+ if (!entry.isFile()) {
988
+ continue;
989
+ }
898
990
  const ext = _pathExtname(entry.name).toLowerCase();
899
- if (!SOURCE_CODE_EXTS.has(ext)) continue;
991
+ if (!SOURCE_CODE_EXTS.has(ext)) {
992
+ continue;
993
+ }
900
994
  const lang = inferLang(entry.name);
901
995
  if (lang) {
902
996
  langCount[lang] = (langCount[lang] || 0) + 1;
903
997
  }
904
998
  }
905
- } catch { /* skip */ }
999
+ } catch {
1000
+ /* skip */
1001
+ }
906
1002
 
907
1003
  let maxLang = 'unknown';
908
1004
  let maxCount = 0;
@@ -920,29 +1016,56 @@ export class ModuleService {
920
1016
  */
921
1017
  #walkProjectForFiles(allFiles, seenPaths, maxFiles) {
922
1018
  const srcDirs = [
923
- 'Sources', 'src', 'lib', 'app', 'pages', 'components', 'modules', 'packages',
924
- 'cmd', 'internal', 'pkg',
1019
+ 'Sources',
1020
+ 'src',
1021
+ 'lib',
1022
+ 'app',
1023
+ 'pages',
1024
+ 'components',
1025
+ 'modules',
1026
+ 'packages',
1027
+ 'cmd',
1028
+ 'internal',
1029
+ 'pkg',
925
1030
  ];
926
1031
 
927
1032
  const walkDir = (dir, targetName) => {
928
- if (allFiles.length >= maxFiles) return;
1033
+ if (allFiles.length >= maxFiles) {
1034
+ return;
1035
+ }
929
1036
  let entries;
930
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
1037
+ try {
1038
+ entries = readdirSync(dir, { withFileTypes: true });
1039
+ } catch {
1040
+ return;
1041
+ }
931
1042
  for (const ent of entries) {
932
- if (allFiles.length >= maxFiles) break;
933
- if (ent.name.startsWith('.')) continue;
1043
+ if (allFiles.length >= maxFiles) {
1044
+ break;
1045
+ }
1046
+ if (ent.name.startsWith('.')) {
1047
+ continue;
1048
+ }
934
1049
  const fp = _pathJoin(dir, ent.name);
935
1050
  if (ent.isDirectory()) {
936
- if (SCAN_EXCLUDE_DIRS.has(ent.name)) continue;
1051
+ if (SCAN_EXCLUDE_DIRS.has(ent.name)) {
1052
+ continue;
1053
+ }
937
1054
  walkDir(fp, targetName);
938
1055
  } else if (ent.isFile() && SOURCE_CODE_EXTS.has(_pathExtname(ent.name).toLowerCase())) {
939
- if (seenPaths.has(fp)) continue;
1056
+ if (seenPaths.has(fp)) {
1057
+ continue;
1058
+ }
940
1059
  seenPaths.add(fp);
941
1060
  try {
942
1061
  const st = statSync(fp);
943
- if (st.size > 512 * 1024) continue;
1062
+ if (st.size > 512 * 1024) {
1063
+ continue;
1064
+ }
944
1065
  const content = readFileSync(fp, 'utf8');
945
- if (content.split('\n').length < 5) continue;
1066
+ if (content.split('\n').length < 5) {
1067
+ continue;
1068
+ }
946
1069
  allFiles.push({
947
1070
  name: ent.name,
948
1071
  path: fp,
@@ -950,7 +1073,9 @@ export class ModuleService {
950
1073
  content,
951
1074
  targetName,
952
1075
  });
953
- } catch { /* unreadable */ }
1076
+ } catch {
1077
+ /* unreadable */
1078
+ }
954
1079
  }
955
1080
  }
956
1081
  };
@@ -9,17 +9,18 @@ import { LanguageService } from '../../shared/LanguageService.js';
9
9
 
10
10
  /* ── V3 必填字段 ── */
11
11
  const REQUIRED_FIELDS = [
12
- 'title', // 中文简短标题
13
- 'trigger', // @前缀 kebab-case
14
- 'category', // View/Service/Tool/Model/Network/Storage/UI/Utility
15
- 'language', // 编程语言标识
16
- 'kind', // rule / pattern / fact
17
- 'doClause', // 英文祈使句正向指令
12
+ 'title', // 中文简短标题
13
+ 'trigger', // @前缀 kebab-case
14
+ 'category', // View/Service/Tool/Model/Network/Storage/UI/Utility
15
+ 'language', // 编程语言标识
16
+ 'kind', // rule / pattern / fact
17
+ 'doClause', // 英文祈使句正向指令
18
18
  'description', // 中文简述 ≤80字
19
19
  ];
20
20
 
21
21
  /* ── 需要 content 子对象有内容 ── */
22
- const REQUIRED_CONTENT_FIELDS = ['pattern', 'markdown', 'rationale'];
22
+ // NOTE: reserved for future content sub-field validation
23
+ // const REQUIRED_CONTENT_FIELDS = ['pattern', 'markdown', 'rationale'];
23
24
 
24
25
  const VALID_CATEGORIES = new Set([
25
26
  'view',
@@ -94,7 +95,9 @@ export class RecipeCandidateValidator {
94
95
 
95
96
  // ── category 合法性 ──
96
97
  if (candidate.category && !VALID_CATEGORIES.has(candidate.category.toLowerCase())) {
97
- warnings.push(`category "${candidate.category}" 不在标准列表(View/Service/Tool/Model/Network/Storage/UI/Utility)`);
98
+ warnings.push(
99
+ `category "${candidate.category}" 不在标准列表(View/Service/Tool/Model/Network/Storage/UI/Utility)`
100
+ );
98
101
  }
99
102
 
100
103
  // ── language 合法性 ──
@@ -659,7 +659,7 @@ export class SearchEngine {
659
659
  }
660
660
 
661
661
  /**
662
- * 补充详细字段(content / description / trigger)— 批量 IN 查询
662
+ * 补充详细字段(content / description / trigger / delivery 字段)— 批量 IN 查询
663
663
  * 用于向量搜索结果与 BM25 结果的一致性
664
664
  */
665
665
  _supplementDetails(items) {
@@ -674,7 +674,8 @@ export class SearchEngine {
674
674
  rows = this.db
675
675
  .prepare(
676
676
  `SELECT id, content, description, trigger, headers, moduleName,
677
- tags, language, category, updatedAt, createdAt, quality, stats, difficulty
677
+ tags, language, category, updatedAt, createdAt, quality, stats, difficulty,
678
+ whenClause, doClause
678
679
  FROM knowledge_entries WHERE id IN (${placeholders})`
679
680
  )
680
681
  .all(...ids);
@@ -694,6 +695,13 @@ export class SearchEngine {
694
695
  if (row.moduleName) {
695
696
  item.moduleName = row.moduleName;
696
697
  }
698
+ // Cursor 交付字段 — 供 Agent 投影生成 actionHint
699
+ if (!item.whenClause && row.whenClause) {
700
+ item.whenClause = row.whenClause;
701
+ }
702
+ if (!item.doClause && row.doClause) {
703
+ item.doClause = row.doClause;
704
+ }
697
705
  // 排序信号补充 — 确保 Funnel/Ranker 有真实数据
698
706
  if (!item.language && row.language) {
699
707
  item.language = row.language;
@@ -11,8 +11,6 @@
11
11
  * factory.generate(spec, 'xcode') — 按 target 生成
12
12
  */
13
13
 
14
- import { XcodeCodec } from './codecs/XcodeCodec.js';
15
-
16
14
  export class SnippetFactory {
17
15
  /** @type {Map<string, import('./codecs/SnippetCodec.js').SnippetCodec>} */
18
16
  #codecs = new Map();
@@ -174,7 +172,9 @@ export class SnippetFactory {
174
172
  #resolveCodec(target) {
175
173
  const codec = this.#codecs.get(target);
176
174
  if (!codec) {
177
- throw new Error(`No codec registered for target "${target}". Available: [${this.getRegisteredTargets().join(', ')}]`);
175
+ throw new Error(
176
+ `No codec registered for target "${target}". Available: [${this.getRegisteredTargets().join(', ')}]`
177
+ );
178
178
  }
179
179
  return codec;
180
180
  }
@@ -8,7 +8,14 @@
8
8
  * 行为由注入的 SnippetCodec 决定。
9
9
  */
10
10
 
11
- import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ readdirSync,
15
+ readFileSync,
16
+ unlinkSync,
17
+ writeFileSync,
18
+ } from 'node:fs';
12
19
  import { join } from 'node:path';
13
20
 
14
21
  export class SnippetInstaller {
@@ -106,8 +113,11 @@ export class SnippetInstaller {
106
113
  for (const spec of specs) {
107
114
  const result = this.install(spec, projectRoot);
108
115
  details.push(result);
109
- if (result.success) successCount++;
110
- else errorCount++;
116
+ if (result.success) {
117
+ successCount++;
118
+ } else {
119
+ errorCount++;
120
+ }
111
121
  }
112
122
 
113
123
  return { success: errorCount === 0, count: recipes.length, successCount, errorCount, details };
@@ -122,13 +132,17 @@ export class SnippetInstaller {
122
132
  */
123
133
  listInstalled(projectRoot) {
124
134
  const dir = this.#resolveDir(projectRoot);
125
- if (!existsSync(dir)) return [];
135
+ if (!existsSync(dir)) {
136
+ return [];
137
+ }
126
138
 
127
139
  const bundleFilename = this.#codec?.getBundleFilename();
128
140
  if (bundleFilename) {
129
141
  // VSCode: 检查 bundle 文件是否存在
130
142
  const bundlePath = join(dir, bundleFilename);
131
- if (!existsSync(bundlePath)) return [];
143
+ if (!existsSync(bundlePath)) {
144
+ return [];
145
+ }
132
146
  try {
133
147
  const content = JSON.parse(readFileSync(bundlePath, 'utf-8'));
134
148
  return Object.keys(content).map((key) => ({
@@ -142,7 +156,11 @@ export class SnippetInstaller {
142
156
 
143
157
  // Xcode: 列出 com.autosnippet.*.codesnippet 文件
144
158
  return readdirSync(dir)
145
- .filter((f) => f.startsWith('com.autosnippet.') && f.endsWith(this.#codec?.fileExtension || '.codesnippet'))
159
+ .filter(
160
+ (f) =>
161
+ f.startsWith('com.autosnippet.') &&
162
+ f.endsWith(this.#codec?.fileExtension || '.codesnippet')
163
+ )
146
164
  .map((f) => ({ filename: f, path: join(dir, f) }));
147
165
  }
148
166
 
@@ -210,15 +228,21 @@ export class SnippetInstaller {
210
228
  // ─────────────── Private ───────────────
211
229
 
212
230
  #assertCodec() {
213
- if (!this.#codec) throw new Error('SnippetCodec not set');
231
+ if (!this.#codec) {
232
+ throw new Error('SnippetCodec not set');
233
+ }
214
234
  }
215
235
 
216
236
  #assertFactory() {
217
- if (!this.#snippetFactory) throw new Error('SnippetFactory not set');
237
+ if (!this.#snippetFactory) {
238
+ throw new Error('SnippetFactory not set');
239
+ }
218
240
  }
219
241
 
220
242
  #resolveDir(projectRoot) {
221
- if (this.#snippetsDirOverride) return this.#snippetsDirOverride;
243
+ if (this.#snippetsDirOverride) {
244
+ return this.#snippetsDirOverride;
245
+ }
222
246
  return this.#codec?.getInstallDir(projectRoot || process.cwd()) || '';
223
247
  }
224
248
 
@@ -247,7 +271,7 @@ export class SnippetInstaller {
247
271
  const entryKey = Object.keys(content)[0];
248
272
  bundle[key] = content[entryKey];
249
273
 
250
- writeFileSync(bundlePath, JSON.stringify(bundle, null, 2) + '\n');
274
+ writeFileSync(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
251
275
  return { success: true, path: bundlePath, message: `Installed: ${key}` };
252
276
  }
253
277
 
@@ -296,7 +320,7 @@ export class SnippetInstaller {
296
320
  return { success: false, message: `Snippet not found in bundle: ${identifier}` };
297
321
  }
298
322
  delete bundle[keyToRemove];
299
- writeFileSync(bundlePath, JSON.stringify(bundle, null, 2) + '\n');
323
+ writeFileSync(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
300
324
  return { success: true, message: `Uninstalled: ${keyToRemove}` };
301
325
  } catch (error) {
302
326
  return { success: false, message: error.message };
@@ -9,8 +9,8 @@
9
9
  */
10
10
 
11
11
  import { join } from 'node:path';
12
- import { SnippetCodec } from './SnippetCodec.js';
13
12
  import { PlaceholderConverter } from '../PlaceholderConverter.js';
13
+ import { SnippetCodec } from './SnippetCodec.js';
14
14
 
15
15
  /** AutoSnippet language → VSCode snippet scope */
16
16
  const VSCODE_LANGUAGE_MAP = {
@@ -58,7 +58,7 @@ export class VSCodeCodec extends SnippetCodec {
58
58
  const key = `Recipe: ${spec.title || spec.identifier}`;
59
59
  bundle[key] = this.#specToEntry(spec);
60
60
  }
61
- return JSON.stringify(bundle, null, 2) + '\n';
61
+ return `${JSON.stringify(bundle, null, 2)}\n`;
62
62
  }
63
63
 
64
64
  /**