autosnippet 3.3.6 → 3.3.7

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 (107) hide show
  1. package/dashboard/dist/assets/icons-FHns2ypa.js +1 -0
  2. package/dashboard/dist/assets/index-BRJv5Y3r.js +135 -0
  3. package/dashboard/dist/assets/index-DzoB7kxK.css +1 -0
  4. package/dashboard/dist/index.html +3 -3
  5. package/dist/bin/cli.js +1 -0
  6. package/dist/lib/agent/AgentRuntime.d.ts +2 -2
  7. package/dist/lib/agent/AgentRuntime.js +26 -18
  8. package/dist/lib/agent/domain/ChatAgentTasks.js +4 -0
  9. package/dist/lib/agent/forced-summary.js +7 -2
  10. package/dist/lib/cli/AiScanService.js +4 -4
  11. package/dist/lib/core/discovery/ConfigWatcher.d.ts +64 -0
  12. package/dist/lib/core/discovery/ConfigWatcher.js +336 -0
  13. package/dist/lib/core/discovery/CustomConfigDiscoverer.d.ts +30 -0
  14. package/dist/lib/core/discovery/CustomConfigDiscoverer.js +1305 -0
  15. package/dist/lib/core/discovery/DiscovererPreference.d.ts +44 -0
  16. package/dist/lib/core/discovery/DiscovererPreference.js +141 -0
  17. package/dist/lib/core/discovery/DiscovererRegistry.d.ts +10 -1
  18. package/dist/lib/core/discovery/DiscovererRegistry.js +42 -2
  19. package/dist/lib/core/discovery/ProjectDiscoverer.d.ts +19 -0
  20. package/dist/lib/core/discovery/index.d.ts +2 -0
  21. package/dist/lib/core/discovery/index.js +4 -0
  22. package/dist/lib/core/discovery/parsers/CMakeParser.d.ts +32 -0
  23. package/dist/lib/core/discovery/parsers/CMakeParser.js +148 -0
  24. package/dist/lib/core/discovery/parsers/GradleDslParser.d.ts +43 -0
  25. package/dist/lib/core/discovery/parsers/GradleDslParser.js +171 -0
  26. package/dist/lib/core/discovery/parsers/JsonConfigParser.d.ts +45 -0
  27. package/dist/lib/core/discovery/parsers/JsonConfigParser.js +122 -0
  28. package/dist/lib/core/discovery/parsers/RubyDslParser.d.ts +49 -0
  29. package/dist/lib/core/discovery/parsers/RubyDslParser.js +282 -0
  30. package/dist/lib/core/discovery/parsers/StarlarkParser.d.ts +33 -0
  31. package/dist/lib/core/discovery/parsers/StarlarkParser.js +229 -0
  32. package/dist/lib/core/discovery/parsers/YamlConfigParser.d.ts +37 -0
  33. package/dist/lib/core/discovery/parsers/YamlConfigParser.js +212 -0
  34. package/dist/lib/domain/knowledge/KnowledgeEntry.d.ts +7 -1
  35. package/dist/lib/domain/knowledge/KnowledgeEntry.js +17 -3
  36. package/dist/lib/external/ai/AiProvider.d.ts +12 -0
  37. package/dist/lib/external/ai/AiProvider.js +24 -0
  38. package/dist/lib/external/ai/AiProviderManager.d.ts +101 -0
  39. package/dist/lib/external/ai/AiProviderManager.js +193 -0
  40. package/dist/lib/external/ai/providers/ClaudeProvider.js +11 -0
  41. package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +18 -0
  42. package/dist/lib/external/ai/providers/MockProvider.d.ts +21 -3
  43. package/dist/lib/external/ai/providers/MockProvider.js +290 -14
  44. package/dist/lib/external/ai/providers/OpenAiProvider.js +16 -0
  45. package/dist/lib/external/lark/LarkTransport.d.ts +5 -1
  46. package/dist/lib/external/lark/LarkTransport.js +10 -2
  47. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.d.ts +20 -0
  48. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.js +432 -0
  49. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +16 -8
  50. package/dist/lib/external/mcp/handlers/bootstrap/refine.js +8 -0
  51. package/dist/lib/external/mcp/handlers/bootstrap-external.d.ts +9 -0
  52. package/dist/lib/external/mcp/handlers/bootstrap-external.js +2 -0
  53. package/dist/lib/external/mcp/handlers/bootstrap-internal.js +2 -0
  54. package/dist/lib/external/mcp/handlers/consolidated.js +2 -1
  55. package/dist/lib/external/mcp/handlers/dimension-complete-external.js +2 -1
  56. package/dist/lib/external/mcp/handlers/evolve-external.js +5 -2
  57. package/dist/lib/external/mcp/handlers/knowledge.js +5 -4
  58. package/dist/lib/http/routes/ai.js +111 -30
  59. package/dist/lib/http/routes/candidates.js +11 -4
  60. package/dist/lib/http/routes/commands.js +10 -1
  61. package/dist/lib/http/routes/health.js +11 -0
  62. package/dist/lib/http/routes/modules.js +27 -0
  63. package/dist/lib/http/routes/recipes.js +7 -0
  64. package/dist/lib/http/utils/routeHelpers.js +2 -1
  65. package/dist/lib/injection/ServiceContainer.d.ts +6 -5
  66. package/dist/lib/injection/ServiceContainer.js +11 -27
  67. package/dist/lib/injection/ServiceMap.d.ts +2 -0
  68. package/dist/lib/injection/modules/AiModule.d.ts +6 -9
  69. package/dist/lib/injection/modules/AiModule.js +82 -39
  70. package/dist/lib/injection/modules/PanoramaModule.js +1 -1
  71. package/dist/lib/service/cleanup/CleanupService.d.ts +54 -7
  72. package/dist/lib/service/cleanup/CleanupService.js +284 -37
  73. package/dist/lib/service/knowledge/CodeEntityGraph.d.ts +6 -0
  74. package/dist/lib/service/knowledge/CodeEntityGraph.js +16 -0
  75. package/dist/lib/service/knowledge/KnowledgeService.js +23 -10
  76. package/dist/lib/service/module/ModuleService.js +10 -19
  77. package/dist/lib/service/panorama/CouplingAnalyzer.d.ts +10 -1
  78. package/dist/lib/service/panorama/CouplingAnalyzer.js +44 -2
  79. package/dist/lib/service/panorama/DimensionAnalyzer.d.ts +1 -1
  80. package/dist/lib/service/panorama/DimensionAnalyzer.js +31 -17
  81. package/dist/lib/service/panorama/LayerInferrer.d.ts +16 -1
  82. package/dist/lib/service/panorama/LayerInferrer.js +118 -1
  83. package/dist/lib/service/panorama/ModuleDiscoverer.d.ts +9 -0
  84. package/dist/lib/service/panorama/ModuleDiscoverer.js +58 -2
  85. package/dist/lib/service/panorama/PanoramaAggregator.d.ts +6 -2
  86. package/dist/lib/service/panorama/PanoramaAggregator.js +84 -6
  87. package/dist/lib/service/panorama/PanoramaScanner.js +28 -0
  88. package/dist/lib/service/panorama/PanoramaService.js +10 -5
  89. package/dist/lib/service/panorama/PanoramaTypes.d.ts +38 -0
  90. package/dist/lib/service/panorama/RoleRefiner.d.ts +2 -0
  91. package/dist/lib/service/panorama/RoleRefiner.js +41 -0
  92. package/dist/lib/service/panorama/TechStackProfiler.d.ts +13 -0
  93. package/dist/lib/service/panorama/TechStackProfiler.js +191 -0
  94. package/dist/lib/service/skills/SignalCollector.d.ts +1 -0
  95. package/dist/lib/service/skills/SignalCollector.js +6 -5
  96. package/dist/lib/service/vector/ContextualEnricher.d.ts +1 -0
  97. package/dist/lib/service/vector/ContextualEnricher.js +4 -0
  98. package/dist/lib/shared/LanguageService.js +3 -0
  99. package/dist/lib/shared/developer-identity.d.ts +18 -0
  100. package/dist/lib/shared/developer-identity.js +62 -0
  101. package/dist/lib/shared/schemas/http-requests.d.ts +8 -17
  102. package/dist/lib/shared/schemas/http-requests.js +9 -6
  103. package/dist/lib/types/knowledge-wire.d.ts +1 -0
  104. package/package.json +1 -1
  105. package/dashboard/dist/assets/icons-D1aVZYFW.js +0 -1
  106. package/dashboard/dist/assets/index-CxHOu8Hd.css +0 -1
  107. package/dashboard/dist/assets/index-DDdAOpYT.js +0 -128
@@ -6,17 +6,20 @@
6
6
  *
7
7
  * 职责:
8
8
  * - AI Provider 自动探测与创建
9
+ * - AiProviderManager 统一管理层
9
10
  * - Embedding fallback provider 管理
10
11
  * - AiFactory 实例注入
11
12
  *
12
13
  * @module AiModule
13
14
  */
15
+ import { AiProviderManager } from '../../external/ai/AiProviderManager.js';
14
16
  /**
15
17
  * 初始化 AI Provider(在模块注册前调用)
16
18
  *
17
19
  * 1. 动态导入 AiFactory
18
20
  * 2. 自动探测可用 AI Provider
19
- * 3. 创建 Embedding fallback(若主 provider 不支持 embedding)
21
+ * 3. 创建 AiProviderManager(统一管理层)
22
+ * 4. 绑定 Token 追踪、Embedding fallback、DI 级联清理
20
23
  */
21
24
  export async function initialize(c) {
22
25
  const logger = c.logger;
@@ -43,56 +46,96 @@ export async function initialize(c) {
43
46
  c.singletons.aiProvider = null;
44
47
  }
45
48
  }
46
- // Embedding fallback provider
47
- initEmbeddingFallback(c);
49
+ // ── 创建 AiProviderManager(统一管理层)──
50
+ const manager = new AiProviderManager(c.singletons.aiProvider || { name: 'mock', model: 'mock-fallback' });
51
+ c.singletons._aiProviderManager = manager;
52
+ // 绑定: DI 数据管道同步(切换时更新 singletons 中的 provider 引用,供工厂函数读取)
53
+ manager._bindDiSync((provider, embed) => {
54
+ c.singletons.aiProvider = provider;
55
+ c.singletons._embedProvider = embed;
56
+ });
57
+ // 绑定: DI 级联清理回调
58
+ manager._bindDependentClearer(() => {
59
+ const cleared = [];
60
+ for (const key of c._aiDependentSingletons || []) {
61
+ if (c.singletons[key]) {
62
+ c.singletons[key] = null;
63
+ cleared.push(key);
64
+ }
65
+ }
66
+ return cleared;
67
+ });
68
+ // 绑定: Embedding fallback 初始化器
69
+ manager._bindEmbedFallbackInit((currentProvider) => {
70
+ return createEmbedFallback(c, currentProvider);
71
+ });
72
+ // Token 追踪 AOP(manager 自身已在构造时 wire,此处延迟注入 recorder)
73
+ // recorder 注入放到 register() 之后(tokenUsageStore 需先注册)
74
+ // Embedding fallback: manager 的 embedFallbackInit 回调已绑定,初始化时主动触发一次
75
+ const initialEmbed = createEmbedFallback(c, c.singletons.aiProvider);
76
+ if (initialEmbed) {
77
+ manager.setEmbedProvider(initialEmbed);
78
+ c.singletons._embedProvider = initialEmbed;
79
+ }
48
80
  }
49
81
  /**
50
- * 创建/刷新 Embedding fallback provider
51
- *
52
- * 若主 provider 不支持 embedding(如 Claude),尝试从其他可用 provider 创建备用。
82
+ * 纯函数: 尝试为给定 provider 创建 Embedding fallback
83
+ * 被 initEmbeddingFallback() 和 AiProviderManager 的 embedFallbackInit 回调共用
53
84
  */
54
- export function initEmbeddingFallback(c) {
55
- const currentProvider = c.singletons.aiProvider;
56
- if ((currentProvider &&
57
- typeof currentProvider
58
- .supportsEmbedding !== 'function') ||
59
- (currentProvider &&
60
- !currentProvider.supportsEmbedding?.())) {
61
- try {
62
- const aiFactory = (c.singletons._aiFactory || {});
63
- const providerName = (currentProvider?.name || '').replace('-', '');
64
- const fbCandidates = typeof aiFactory.getAvailableFallbacks === 'function'
65
- ? aiFactory.getAvailableFallbacks(providerName)
66
- : [];
67
- for (const fb of fbCandidates) {
68
- try {
69
- const fbProvider = aiFactory.createProvider({ provider: fb });
70
- if (typeof fbProvider.supportsEmbedding === 'function' &&
71
- fbProvider.supportsEmbedding()) {
72
- c.singletons._embedProvider = fbProvider;
73
- c.logger.info('Embedding fallback provider created', { provider: fb });
74
- break;
75
- }
76
- }
77
- catch {
78
- /* skip */
85
+ function createEmbedFallback(c, currentProvider) {
86
+ if (!currentProvider ||
87
+ (typeof currentProvider.supportsEmbedding === 'function' && currentProvider.supportsEmbedding())) {
88
+ return null; // 主 provider 已支持 embedding,无需 fallback
89
+ }
90
+ try {
91
+ const aiFactory = (c.singletons._aiFactory || {});
92
+ const providerName = (currentProvider.name || '').replace('-', '');
93
+ const fbCandidates = typeof aiFactory.getAvailableFallbacks === 'function'
94
+ ? aiFactory.getAvailableFallbacks(providerName)
95
+ : [];
96
+ for (const fb of fbCandidates) {
97
+ try {
98
+ const fbProvider = aiFactory.createProvider?.({ provider: fb });
99
+ if (fbProvider &&
100
+ typeof fbProvider.supportsEmbedding === 'function' &&
101
+ fbProvider.supportsEmbedding()) {
102
+ c.logger.info('Embedding fallback provider created', { provider: fb });
103
+ return fbProvider;
79
104
  }
80
105
  }
81
- }
82
- catch {
83
- /* no embed fallback available */
106
+ catch {
107
+ /* skip */
108
+ }
84
109
  }
85
110
  }
111
+ catch {
112
+ /* no embed fallback available */
113
+ }
114
+ return null;
86
115
  }
87
116
  /**
88
117
  * 注册 AI 相关的服务到容器
89
118
  *
90
- * 当前 AI Provider 和 AiFactory 通过 singletons 直接管理,
91
- * 此方法注册便于其他模块通过 container.get() 获取的快捷服务。
119
+ * - 标记 AI 模块就绪
120
+ * - 注册 aiProviderManager 服务
121
+ * - 延迟注入 TokenRecorder(tokenUsageStore 此时已可用)
92
122
  */
93
123
  export function register(c) {
94
- // aiProvider 和 _aiFactory 已通过 initialize() 写入 singletons
95
- // KnowledgeModule 中已注册 'aiProvider' 的 register 工厂
96
- // 此处仅标记 AI 模块已就绪
97
124
  c.singletons._aiModuleReady = true;
125
+ // 注册 aiProviderManager(消费者通过 container.get('aiProviderManager') 获取)
126
+ c.register('aiProviderManager', () => c.singletons._aiProviderManager);
127
+ // 延迟注入 TokenRecorder 到 manager(tokenUsageStore 在 AppModule 中注册)
128
+ const manager = c.singletons._aiProviderManager;
129
+ const containerRef = c;
130
+ manager.setTokenRecorder({
131
+ record(r) {
132
+ try {
133
+ const store = containerRef.get('tokenUsageStore');
134
+ store.record(r);
135
+ }
136
+ catch {
137
+ /* tokenUsageStore not available yet */
138
+ }
139
+ },
140
+ });
98
141
  }
@@ -32,7 +32,7 @@ export const PanoramaModule = {
32
32
  ct.singleton('roleRefiner', () => new RoleRefiner(getDb(), getProjectRoot()));
33
33
  ct.singleton('couplingAnalyzer', () => new CouplingAnalyzer(getDb(), getProjectRoot()));
34
34
  ct.singleton('layerInferrer', () => new LayerInferrer());
35
- ct.singleton('dimensionAnalyzer', () => new DimensionAnalyzer(getDb()));
35
+ ct.singleton('dimensionAnalyzer', () => new DimensionAnalyzer(getDb(), getProjectRoot()));
36
36
  ct.singleton('panoramaAggregator', (c) => {
37
37
  const sc = c;
38
38
  const roleRefiner = sc.get('roleRefiner');
@@ -1,12 +1,20 @@
1
1
  /**
2
- * CleanupService — 统一数据清理策略
2
+ * CleanupService — 统一数据清理策略(垃圾桶模式)
3
3
  *
4
4
  * 提供两种清理模式:
5
- * - fullReset(): 全量清理(删除一切知识/缓存/衍生数据),用于 bootstrap 冷启动
6
- * - rescanClean(): Rescan 清理(保留 Recipe,清除衍生缓存),用于增量知识更新
5
+ * - fullReset(): 全量清理 将旧数据打包到时间戳垃圾桶文件夹,DB 表清空
6
+ * - rescanClean(): Rescan 清理 — 保留 Recipe,清除衍生缓存
7
7
  * - snapshotRecipes(): 快照当前活跃 Recipe 信息
8
+ * - purgeExpiredTrash(): 清除超时限的垃圾桶文件夹
8
9
  *
9
- * 设计原则:
10
+ * 垃圾桶设计:
11
+ * - 位于 .autosnippet/.trash/<ISO-timestamp>/ 下
12
+ * - fullReset 时先将 candidates/ recipes/ skills/ wiki/ 移入垃圾桶,再清 DB
13
+ * - DB 数据导出为 db-snapshot.jsonl 保存在垃圾桶内
14
+ * - 超过保留天数(默认 7 天)的垃圾桶在下次 fullReset 或服务启动时自动清除
15
+ * - 暂不提供恢复功能(需要 merge 处理过于复杂)
16
+ *
17
+ * 保留原则:
10
18
  * - 配置数据 (config.json, constitution.yaml, boxspec.json) 永不清理
11
19
  * - IDE 集成配置 (.vscode/, .cursor/, .github/) 永不清理
12
20
  * - 交付物 (.cursor/rules/autosnippet-*) 由 R4 重建,不在此清理
@@ -24,6 +32,22 @@ export interface CleanupResult {
24
32
  clearedTables: string[];
25
33
  preservedRecipes: number;
26
34
  errors: string[];
35
+ /** 垃圾桶信息(fullReset 时填充) */
36
+ trash?: {
37
+ /** 垃圾桶文件夹路径 */
38
+ folder: string;
39
+ /** 移入垃圾桶的文件/目录数 */
40
+ movedItems: number;
41
+ /** DB 快照行数 */
42
+ dbSnapshotRows: number;
43
+ };
44
+ /** 本次清除的过期垃圾桶 */
45
+ purgedTrash?: {
46
+ /** 清除的垃圾桶数 */
47
+ count: number;
48
+ /** 释放的磁盘空间估算 (bytes) */
49
+ freedBytes: number;
50
+ };
27
51
  }
28
52
  /** Recipe 快照条目 */
29
53
  export interface RecipeSnapshotEntry {
@@ -60,10 +84,16 @@ export declare class CleanupService {
60
84
  /** 更新 DB 引用(fullReset 后重连时调用) */
61
85
  setDb(db: unknown): void;
62
86
  /**
63
- * 全量清理 — 用于 bootstrap 冷启动
87
+ * 全量清理 — 用于 bootstrap 冷启动(垃圾桶模式)
88
+ *
89
+ * 流程:
90
+ * 1. 先清除过期垃圾桶(超过 TRASH_RETENTION_DAYS)
91
+ * 2. 创建时间戳垃圾桶文件夹
92
+ * 3. 将 candidates/ recipes/ skills/ wiki/ 移入垃圾桶
93
+ * 4. 导出 DB 关键表数据到 db-snapshot.jsonl
94
+ * 5. 清空 DB 所有数据表
95
+ * 6. 清除向量索引、bootstrap-report、logs 等缓存
64
96
  *
65
- * 清除: DB 所有数据表、candidates/、recipes/、skills/、wiki/、
66
- * 向量索引、bootstrap-report.json、logs/signals/
67
97
  * 保留: config.json、constitution.yaml、boxspec.json、IDE 配置
68
98
  */
69
99
  fullReset(): Promise<CleanupResult>;
@@ -81,5 +111,22 @@ export declare class CleanupService {
81
111
  * 用于 rescan 前记录保留的知识条目
82
112
  */
83
113
  snapshotRecipes(): Promise<RecipeSnapshot>;
114
+ /**
115
+ * 清除超过保留期限的垃圾桶文件夹
116
+ * 可在服务启动时或 fullReset 前调用
117
+ */
118
+ purgeExpiredTrash(): {
119
+ count: number;
120
+ freedBytes: number;
121
+ folders: string[];
122
+ };
123
+ /**
124
+ * 列出当前所有垃圾桶(供 Dashboard 展示)
125
+ */
126
+ listTrashFolders(): Array<{
127
+ name: string;
128
+ createdAt: Date;
129
+ sizeMB: number;
130
+ }>;
84
131
  }
85
132
  export {};
@@ -1,12 +1,20 @@
1
1
  /**
2
- * CleanupService — 统一数据清理策略
2
+ * CleanupService — 统一数据清理策略(垃圾桶模式)
3
3
  *
4
4
  * 提供两种清理模式:
5
- * - fullReset(): 全量清理(删除一切知识/缓存/衍生数据),用于 bootstrap 冷启动
6
- * - rescanClean(): Rescan 清理(保留 Recipe,清除衍生缓存),用于增量知识更新
5
+ * - fullReset(): 全量清理 将旧数据打包到时间戳垃圾桶文件夹,DB 表清空
6
+ * - rescanClean(): Rescan 清理 — 保留 Recipe,清除衍生缓存
7
7
  * - snapshotRecipes(): 快照当前活跃 Recipe 信息
8
+ * - purgeExpiredTrash(): 清除超时限的垃圾桶文件夹
8
9
  *
9
- * 设计原则:
10
+ * 垃圾桶设计:
11
+ * - 位于 .autosnippet/.trash/<ISO-timestamp>/ 下
12
+ * - fullReset 时先将 candidates/ recipes/ skills/ wiki/ 移入垃圾桶,再清 DB
13
+ * - DB 数据导出为 db-snapshot.jsonl 保存在垃圾桶内
14
+ * - 超过保留天数(默认 7 天)的垃圾桶在下次 fullReset 或服务启动时自动清除
15
+ * - 暂不提供恢复功能(需要 merge 处理过于复杂)
16
+ *
17
+ * 保留原则:
10
18
  * - 配置数据 (config.json, constitution.yaml, boxspec.json) 永不清理
11
19
  * - IDE 集成配置 (.vscode/, .cursor/, .github/) 永不清理
12
20
  * - 交付物 (.cursor/rules/autosnippet-*) 由 R4 重建,不在此清理
@@ -18,36 +26,52 @@ import path from 'node:path';
18
26
  import { CANDIDATES_DIR } from '#infra/config/Defaults.js';
19
27
  import { getContextIndexPath, getProjectKnowledgePath, getProjectRecipesPath, getProjectSkillsPath, } from '#infra/config/Paths.js';
20
28
  // ── 常量 ────────────────────────────────────────────────────
21
- /** fullReset 时清除的所有 DB 表(不含 schema_migrations) */
29
+ /** 垃圾桶根目录(相对于 .autosnippet/) */
30
+ const TRASH_DIR = '.trash';
31
+ /** 垃圾桶保留天数,超过后自动 purge */
32
+ const TRASH_RETENTION_DAYS = 7;
33
+ /** DB 快照文件名 */
34
+ const DB_SNAPSHOT_FILE = 'db-snapshot.jsonl';
35
+ /**
36
+ * fullReset 时清除的所有 DB 表(不含 schema_migrations)
37
+ *
38
+ * ⚠️ 顺序重要:子表必须排在父表之前,否则 FK 约束会阻止 DELETE。
39
+ * lifecycle_transition_events → knowledge_entries, evolution_proposals
40
+ * evolution_proposals → knowledge_entries
41
+ * recipe_source_refs → knowledge_entries (CASCADE)
42
+ * bootstrap_dim_files → bootstrap_snapshots (CASCADE)
43
+ */
22
44
  const ALL_DATA_TABLES = [
23
- 'knowledge_entries',
45
+ // ── FK 子表先删 ──
46
+ 'lifecycle_transition_events',
47
+ 'recipe_source_refs',
48
+ 'evolution_proposals',
24
49
  'knowledge_edges',
50
+ 'bootstrap_dim_files',
51
+ // ── 父表后删 ──
52
+ 'knowledge_entries',
53
+ 'bootstrap_snapshots',
54
+ // ── 无 FK 依赖 ──
25
55
  'guard_violations',
26
56
  'audit_logs',
27
57
  'sessions',
28
- 'token_usage',
29
58
  'semantic_memories',
30
- 'bootstrap_snapshots',
31
- 'bootstrap_dim_files',
32
59
  'code_entities',
33
60
  'remote_commands',
34
61
  'remote_state',
35
- 'evolution_proposals',
36
- 'recipe_source_refs',
37
62
  ];
38
63
  /** rescanClean 时清除的 DB 表(保留知识/进化相关表) */
39
64
  const RESCAN_CLEAN_TABLES = [
65
+ 'bootstrap_dim_files', // FK → bootstrap_snapshots, 先删
66
+ 'recipe_source_refs', // FK → knowledge_entries, 先删
67
+ 'bootstrap_snapshots',
40
68
  'code_entities',
41
69
  'guard_violations',
42
- 'bootstrap_snapshots',
43
- 'bootstrap_dim_files',
44
70
  'semantic_memories',
45
71
  'sessions',
46
72
  'audit_logs',
47
- 'token_usage',
48
73
  'remote_commands',
49
74
  'remote_state',
50
- 'recipe_source_refs',
51
75
  ];
52
76
  // ── CleanupService ──────────────────────────────────────────
53
77
  export class CleanupService {
@@ -71,12 +95,18 @@ export class CleanupService {
71
95
  : db
72
96
  : null;
73
97
  }
74
- // ─── 需求 A:全量清理(删除一切) ─────────────────────
98
+ // ─── 需求 A:全量清理(垃圾桶模式) ────────────────────
75
99
  /**
76
- * 全量清理 — 用于 bootstrap 冷启动
100
+ * 全量清理 — 用于 bootstrap 冷启动(垃圾桶模式)
101
+ *
102
+ * 流程:
103
+ * 1. 先清除过期垃圾桶(超过 TRASH_RETENTION_DAYS)
104
+ * 2. 创建时间戳垃圾桶文件夹
105
+ * 3. 将 candidates/ recipes/ skills/ wiki/ 移入垃圾桶
106
+ * 4. 导出 DB 关键表数据到 db-snapshot.jsonl
107
+ * 5. 清空 DB 所有数据表
108
+ * 6. 清除向量索引、bootstrap-report、logs 等缓存
77
109
  *
78
- * 清除: DB 所有数据表、candidates/、recipes/、skills/、wiki/、
79
- * 向量索引、bootstrap-report.json、logs/signals/
80
110
  * 保留: config.json、constitution.yaml、boxspec.json、IDE 配置
81
111
  */
82
112
  async fullReset() {
@@ -86,8 +116,34 @@ export class CleanupService {
86
116
  preservedRecipes: 0,
87
117
  errors: [],
88
118
  };
89
- this.#logger.info('[CleanupService] Starting fullReset...');
90
- // 1. 清除 DB 所有数据表
119
+ this.#logger.info('[CleanupService] Starting fullReset (trash-bin mode)...');
120
+ // 0. 清除过期垃圾桶
121
+ const purged = this.#purgeExpiredTrash();
122
+ if (purged.count > 0) {
123
+ result.purgedTrash = purged;
124
+ this.#logger.info(`[CleanupService] Purged ${purged.count} expired trash folders`);
125
+ }
126
+ // 1. 创建时间戳垃圾桶文件夹
127
+ const trashFolder = this.#createTrashFolder();
128
+ let movedItems = 0;
129
+ let dbSnapshotRows = 0;
130
+ // 2. 将知识目录移入垃圾桶(move 而非 copy,速度快)
131
+ const kbPath = getProjectKnowledgePath(this.#projectRoot);
132
+ const dirsToTrash = [
133
+ { src: path.join(this.#projectRoot, CANDIDATES_DIR), name: 'candidates' },
134
+ { src: getProjectRecipesPath(this.#projectRoot), name: 'recipes' },
135
+ { src: getProjectSkillsPath(this.#projectRoot), name: 'skills' },
136
+ { src: path.join(kbPath, 'wiki'), name: 'wiki' },
137
+ ];
138
+ for (const { src, name } of dirsToTrash) {
139
+ const moved = this.#moveToTrash(src, path.join(trashFolder, name));
140
+ movedItems += moved;
141
+ }
142
+ // 3. 导出 DB 数据到垃圾桶(JSONL 格式,每行一个 {table, row})
143
+ if (this.#db) {
144
+ dbSnapshotRows = this.#exportDbToTrash(trashFolder);
145
+ }
146
+ // 4. 清空 DB 所有数据表
91
147
  if (this.#db) {
92
148
  for (const table of ALL_DATA_TABLES) {
93
149
  try {
@@ -96,14 +152,14 @@ export class CleanupService {
96
152
  }
97
153
  catch (err) {
98
154
  const msg = err instanceof Error ? err.message : String(err);
99
- // 表可能不存在(未 migrate),跳过
100
155
  if (!msg.includes('no such table')) {
101
156
  result.errors.push(`Failed to clear ${table}: ${msg}`);
157
+ this.#logger.warn(`[CleanupService] DELETE FROM ${table} failed: ${msg}`);
102
158
  }
103
159
  }
104
160
  }
105
- // 也清除 tasks 相关表(来自 migration 002
106
- for (const table of ['tasks', 'task_dependencies', 'task_events']) {
161
+ // tasks 相关表(来自 migration 002,需先删子表)
162
+ for (const table of ['task_events', 'task_dependencies', 'tasks']) {
107
163
  try {
108
164
  this.#db.exec(`DELETE FROM ${table}`);
109
165
  result.clearedTables.push(table);
@@ -113,23 +169,30 @@ export class CleanupService {
113
169
  }
114
170
  }
115
171
  }
116
- // 2. 清空 candidates/ 目录
117
- result.deletedFiles += this.#clearDirectory(path.join(this.#projectRoot, CANDIDATES_DIR));
118
- // 3. 清空 recipes/ 目录
119
- result.deletedFiles += this.#clearDirectory(getProjectRecipesPath(this.#projectRoot));
120
- // 4. 清空 skills/ 目录
121
- result.deletedFiles += this.#clearDirectory(getProjectSkillsPath(this.#projectRoot));
122
- // 5. 清空 wiki/ 目录
123
- result.deletedFiles += this.#clearDirectory(path.join(getProjectKnowledgePath(this.#projectRoot), 'wiki'));
124
- // 6. 删除向量索引
172
+ else {
173
+ this.#logger.warn('[CleanupService] No database reference — DB tables NOT cleared!');
174
+ result.errors.push('DB reference is null, database tables were not cleared');
175
+ }
176
+ // 5. 重建被移走的空目录(bootstrap 后续步骤需要)
177
+ for (const { src } of dirsToTrash) {
178
+ if (!fs.existsSync(src)) {
179
+ fs.mkdirSync(src, { recursive: true });
180
+ }
181
+ }
182
+ // 6. 清除向量索引
125
183
  result.deletedFiles += this.#clearDirectory(getContextIndexPath(this.#projectRoot));
126
184
  // 7. 删除 bootstrap-report.json
127
- result.deletedFiles += this.#deleteFile(path.join(getProjectKnowledgePath(this.#projectRoot), '.autosnippet', 'bootstrap-report.json'));
185
+ result.deletedFiles += this.#deleteFile(path.join(kbPath, '.autosnippet', 'bootstrap-report.json'));
128
186
  // 8. 清除 logs/signals/
129
- result.deletedFiles += this.#clearDirectory(path.join(getProjectKnowledgePath(this.#projectRoot), '.autosnippet', 'logs', 'signals'));
130
- this.#logger.info('[CleanupService] fullReset complete', {
187
+ result.deletedFiles += this.#clearDirectory(path.join(kbPath, '.autosnippet', 'logs', 'signals'));
188
+ result.deletedFiles += movedItems;
189
+ result.trash = { folder: trashFolder, movedItems, dbSnapshotRows };
190
+ this.#logger.info('[CleanupService] fullReset complete (trash-bin mode)', {
191
+ trashFolder: path.basename(trashFolder),
192
+ movedItems,
193
+ dbSnapshotRows,
131
194
  tables: result.clearedTables.length,
132
- files: result.deletedFiles,
195
+ purgedExpired: purged.count,
133
196
  errors: result.errors.length,
134
197
  });
135
198
  return result;
@@ -268,7 +331,191 @@ export class CleanupService {
268
331
  return { count: 0, entries: [], coverageByDimension: {} };
269
332
  }
270
333
  }
334
+ // ─── 垃圾桶管理 ───────────────────────────────────────
335
+ /**
336
+ * 清除超过保留期限的垃圾桶文件夹
337
+ * 可在服务启动时或 fullReset 前调用
338
+ */
339
+ purgeExpiredTrash() {
340
+ return this.#purgeExpiredTrash();
341
+ }
342
+ /**
343
+ * 列出当前所有垃圾桶(供 Dashboard 展示)
344
+ */
345
+ listTrashFolders() {
346
+ const trashRoot = this.#getTrashRoot();
347
+ if (!fs.existsSync(trashRoot)) {
348
+ return [];
349
+ }
350
+ const entries = fs.readdirSync(trashRoot).sort().reverse();
351
+ return entries
352
+ .filter((name) => /^\d{4}-\d{2}-\d{2}T/.test(name))
353
+ .map((name) => {
354
+ const fullPath = path.join(trashRoot, name);
355
+ const stat = fs.statSync(fullPath);
356
+ return {
357
+ name,
358
+ createdAt: stat.birthtime,
359
+ sizeMB: Math.round((this.#getDirSize(fullPath) / 1024 / 1024) * 100) / 100,
360
+ };
361
+ });
362
+ }
271
363
  // ─── 内部工具方法 ─────────────────────────────────────
364
+ /** 获取垃圾桶根目录 (.autosnippet/.trash/) */
365
+ #getTrashRoot() {
366
+ return path.join(this.#projectRoot, '.autosnippet', TRASH_DIR);
367
+ }
368
+ /** 创建时间戳垃圾桶文件夹,返回绝对路径 */
369
+ #createTrashFolder() {
370
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
371
+ const trashFolder = path.join(this.#getTrashRoot(), ts);
372
+ fs.mkdirSync(trashFolder, { recursive: true });
373
+ return trashFolder;
374
+ }
375
+ /**
376
+ * 将源目录内容移入垃圾桶对应子目录
377
+ * 使用 rename 实现(同文件系统内是原子操作,速度极快)
378
+ * @returns 移动的顶层条目数
379
+ */
380
+ #moveToTrash(srcDir, trashSubDir) {
381
+ if (!fs.existsSync(srcDir)) {
382
+ return 0;
383
+ }
384
+ const entries = fs.readdirSync(srcDir);
385
+ if (entries.length === 0) {
386
+ return 0;
387
+ }
388
+ fs.mkdirSync(trashSubDir, { recursive: true });
389
+ let count = 0;
390
+ for (const entry of entries) {
391
+ const src = path.join(srcDir, entry);
392
+ const dest = path.join(trashSubDir, entry);
393
+ try {
394
+ fs.renameSync(src, dest);
395
+ count++;
396
+ }
397
+ catch {
398
+ // rename 可能跨设备失败,fallback 到 copy+delete
399
+ try {
400
+ fs.cpSync(src, dest, { recursive: true });
401
+ fs.rmSync(src, { recursive: true, force: true });
402
+ count++;
403
+ }
404
+ catch (err) {
405
+ const msg = err instanceof Error ? err.message : String(err);
406
+ this.#logger.warn(`[CleanupService] Failed to move ${entry} to trash: ${msg}`);
407
+ }
408
+ }
409
+ }
410
+ return count;
411
+ }
412
+ /**
413
+ * 导出 DB 关键表数据到垃圾桶(JSONL 格式)
414
+ * 只导出有实际业务数据的表,跳过纯缓存表
415
+ */
416
+ #exportDbToTrash(trashFolder) {
417
+ if (!this.#db) {
418
+ return 0;
419
+ }
420
+ const tablesToExport = [
421
+ 'knowledge_entries',
422
+ 'knowledge_edges',
423
+ 'lifecycle_transition_events',
424
+ 'evolution_proposals',
425
+ 'recipe_source_refs',
426
+ 'guard_violations',
427
+ ];
428
+ const snapshotPath = path.join(trashFolder, DB_SNAPSHOT_FILE);
429
+ let totalRows = 0;
430
+ const lines = [];
431
+ for (const table of tablesToExport) {
432
+ try {
433
+ const rows = this.#db.prepare(`SELECT * FROM ${table}`).all();
434
+ for (const row of rows) {
435
+ lines.push(JSON.stringify({ _table: table, ...row }));
436
+ totalRows++;
437
+ }
438
+ }
439
+ catch {
440
+ // 表可能不存在,跳过
441
+ }
442
+ }
443
+ if (lines.length > 0) {
444
+ fs.writeFileSync(snapshotPath, `${lines.join('\n')}\n`, 'utf-8');
445
+ this.#logger.info(`[CleanupService] DB snapshot: ${totalRows} rows → ${DB_SNAPSHOT_FILE}`);
446
+ }
447
+ return totalRows;
448
+ }
449
+ /** 清除过期垃圾桶文件夹 */
450
+ #purgeExpiredTrash() {
451
+ const trashRoot = this.#getTrashRoot();
452
+ if (!fs.existsSync(trashRoot)) {
453
+ return { count: 0, freedBytes: 0, folders: [] };
454
+ }
455
+ const now = Date.now();
456
+ const maxAge = TRASH_RETENTION_DAYS * 24 * 60 * 60 * 1000;
457
+ const entries = fs.readdirSync(trashRoot);
458
+ let count = 0;
459
+ let freedBytes = 0;
460
+ const folders = [];
461
+ for (const entry of entries) {
462
+ const fullPath = path.join(trashRoot, entry);
463
+ try {
464
+ const stat = fs.statSync(fullPath);
465
+ if (!stat.isDirectory()) {
466
+ continue;
467
+ }
468
+ // 从文件夹名解析时间戳(格式: 2026-04-09T14-30-00-000Z)
469
+ const ts = entry.replace(/-(\d{2})-(\d{2})-(\d{3}Z)$/, ':$1:$2.$3');
470
+ const created = new Date(ts).getTime();
471
+ const age = now - (Number.isNaN(created) ? stat.birthtimeMs : created);
472
+ if (age > maxAge) {
473
+ const size = this.#getDirSize(fullPath);
474
+ fs.rmSync(fullPath, { recursive: true, force: true });
475
+ freedBytes += size;
476
+ count++;
477
+ folders.push(entry);
478
+ this.#logger.info(`[CleanupService] Purged expired trash: ${entry} (${Math.round(size / 1024)}KB)`);
479
+ }
480
+ }
481
+ catch (err) {
482
+ const msg = err instanceof Error ? err.message : String(err);
483
+ this.#logger.warn(`[CleanupService] Failed to purge trash ${entry}: ${msg}`);
484
+ }
485
+ }
486
+ // 如果垃圾桶根目录为空,也删掉
487
+ try {
488
+ const remaining = fs.readdirSync(trashRoot);
489
+ if (remaining.length === 0) {
490
+ fs.rmdirSync(trashRoot);
491
+ }
492
+ }
493
+ catch {
494
+ /* ignore */
495
+ }
496
+ return { count, freedBytes, folders };
497
+ }
498
+ /** 递归计算目录大小 (bytes) */
499
+ #getDirSize(dirPath) {
500
+ let size = 0;
501
+ try {
502
+ const entries = fs.readdirSync(dirPath);
503
+ for (const entry of entries) {
504
+ const fullPath = path.join(dirPath, entry);
505
+ const stat = fs.statSync(fullPath);
506
+ if (stat.isDirectory()) {
507
+ size += this.#getDirSize(fullPath);
508
+ }
509
+ else {
510
+ size += stat.size;
511
+ }
512
+ }
513
+ }
514
+ catch {
515
+ /* ignore */
516
+ }
517
+ return size;
518
+ }
272
519
  /**
273
520
  * 清空目录内容(保留目录本身)
274
521
  * @returns 删除的文件数
@@ -85,6 +85,12 @@ interface DepGraphNode {
85
85
  id?: string;
86
86
  label?: string;
87
87
  type?: string;
88
+ layer?: string;
89
+ version?: string;
90
+ group?: string;
91
+ fullPath?: string;
92
+ indirect?: boolean;
93
+ [key: string]: unknown;
88
94
  }
89
95
  interface DepGraphData {
90
96
  nodes?: (DepGraphNode | string)[];