autosnippet 3.3.2 → 3.3.3

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 (51) hide show
  1. package/dist/bin/cli.js +27 -1
  2. package/dist/lib/cli/KnowledgeSyncService.d.ts +26 -0
  3. package/dist/lib/cli/KnowledgeSyncService.js +33 -1
  4. package/dist/lib/external/mcp/handlers/browse.d.ts +1 -0
  5. package/dist/lib/external/mcp/handlers/browse.js +2 -1
  6. package/dist/lib/external/mcp/handlers/consolidated.d.ts +1 -0
  7. package/dist/lib/external/mcp/handlers/panorama.d.ts +11 -11
  8. package/dist/lib/external/mcp/handlers/panorama.js +20 -20
  9. package/dist/lib/external/mcp/handlers/system.d.ts +1 -1
  10. package/dist/lib/external/mcp/handlers/task.js +2 -1
  11. package/dist/lib/external/mcp/tools.d.ts +12 -12
  12. package/dist/lib/external/mcp/tools.js +120 -118
  13. package/dist/lib/http/middleware/validate.js +7 -3
  14. package/dist/lib/infrastructure/database/drizzle/schema.d.ts +100 -0
  15. package/dist/lib/infrastructure/database/drizzle/schema.js +10 -0
  16. package/dist/lib/infrastructure/database/migrations/005_recipe_source_refs.d.ts +9 -0
  17. package/dist/lib/infrastructure/database/migrations/005_recipe_source_refs.js +24 -0
  18. package/dist/lib/infrastructure/vector/HnswVectorAdapter.js +18 -2
  19. package/dist/lib/injection/ServiceContainer.js +2 -0
  20. package/dist/lib/injection/modules/KnowledgeModule.d.ts +5 -0
  21. package/dist/lib/injection/modules/KnowledgeModule.js +80 -0
  22. package/dist/lib/service/bootstrap/UiStartupTasks.d.ts +45 -0
  23. package/dist/lib/service/bootstrap/UiStartupTasks.js +101 -0
  24. package/dist/lib/service/evolution/ConsolidationAdvisor.js +9 -9
  25. package/dist/lib/service/evolution/ContradictionDetector.js +2 -2
  26. package/dist/lib/service/evolution/RedundancyAnalyzer.js +2 -2
  27. package/dist/lib/service/knowledge/SourceRefReconciler.d.ts +68 -0
  28. package/dist/lib/service/knowledge/SourceRefReconciler.js +309 -0
  29. package/dist/lib/service/panorama/PanoramaService.d.ts +18 -1
  30. package/dist/lib/service/panorama/PanoramaService.js +148 -5
  31. package/dist/lib/service/search/BM25Scorer.d.ts +2 -2
  32. package/dist/lib/service/search/CoarseRanker.d.ts +7 -6
  33. package/dist/lib/service/search/CoarseRanker.js +11 -10
  34. package/dist/lib/service/search/FieldWeightedScorer.d.ts +81 -0
  35. package/dist/lib/service/search/FieldWeightedScorer.js +318 -0
  36. package/dist/lib/service/search/MultiSignalRanker.d.ts +2 -2
  37. package/dist/lib/service/search/MultiSignalRanker.js +1 -1
  38. package/dist/lib/service/search/SearchEngine.d.ts +8 -7
  39. package/dist/lib/service/search/SearchEngine.js +59 -10
  40. package/dist/lib/service/search/SearchTypes.d.ts +23 -3
  41. package/dist/lib/service/search/SearchTypes.js +6 -1
  42. package/dist/lib/service/task/IntentExtractor.d.ts +8 -0
  43. package/dist/lib/service/task/IntentExtractor.js +115 -1
  44. package/dist/lib/service/task/PrimeSearchPipeline.js +39 -24
  45. package/dist/lib/service/vector/VectorService.d.ts +3 -0
  46. package/dist/lib/service/vector/VectorService.js +38 -4
  47. package/package.json +1 -1
  48. package/skills/autosnippet-create/SKILL.md +98 -89
  49. package/skills/autosnippet-devdocs/SKILL.md +55 -60
  50. package/templates/guard-ci.yml +2 -2
  51. package/templates/recipes-setup/_template.md +39 -39
@@ -172,3 +172,83 @@ export function register(c) {
172
172
  return new ConsolidationAdvisor(db.getDb());
173
173
  });
174
174
  }
175
+ /**
176
+ * 初始化知识服务(在容器初始化后调用)
177
+ * 绑定 EventBus → SearchEngine.refreshIndex() + recipe_source_refs 填充
178
+ */
179
+ export function initializeKnowledgeServices(c) {
180
+ if (!c.services.eventBus || !c.services.searchEngine) {
181
+ return;
182
+ }
183
+ try {
184
+ const { EventBus } = await_import_EventBus();
185
+ const eventBus = c.get('eventBus');
186
+ const searchEngine = c.get('searchEngine');
187
+ // Bug 修复: BM25 索引与 Vector 索引一致性 — 将 knowledge:changed 事件绑定到 refreshIndex
188
+ eventBus.on('knowledge:changed', () => {
189
+ try {
190
+ searchEngine.refreshIndex();
191
+ }
192
+ catch {
193
+ /* refreshIndex failure is non-fatal */
194
+ }
195
+ });
196
+ // recipe_source_refs 填充:MCP 内提交新知识后同步更新桥接表
197
+ eventBus.on('knowledge:changed', (data) => {
198
+ try {
199
+ const d = data;
200
+ if (d.action === 'create' && d.entryId) {
201
+ _populateSourceRefsForEntry(c, d.entryId);
202
+ }
203
+ }
204
+ catch {
205
+ /* sourceRef population failure is non-fatal */
206
+ }
207
+ });
208
+ }
209
+ catch {
210
+ /* EventBus/SearchEngine not available — skip binding */
211
+ }
212
+ }
213
+ /** EventBus 延迟引用(避免循环依赖) */
214
+ function await_import_EventBus() {
215
+ // EventBus 类型已经通过 container 解析,此处只用于 TS 类型
216
+ return {
217
+ EventBus: Object,
218
+ };
219
+ }
220
+ /**
221
+ * 从 knowledge_entries.reasoning 中提取 sources 并填充 recipe_source_refs 桥接表
222
+ */
223
+ function _populateSourceRefsForEntry(c, entryId) {
224
+ const db = c.get('database');
225
+ const rawDb = db.getDb();
226
+ const row = rawDb.prepare(`SELECT reasoning FROM knowledge_entries WHERE id = ?`).get(entryId);
227
+ if (!row?.reasoning) {
228
+ return;
229
+ }
230
+ let sources = [];
231
+ try {
232
+ const reasoning = JSON.parse(row.reasoning);
233
+ sources = Array.isArray(reasoning.sources)
234
+ ? reasoning.sources.filter((s) => typeof s === 'string' && s.length > 0)
235
+ : [];
236
+ }
237
+ catch {
238
+ return;
239
+ }
240
+ if (sources.length === 0) {
241
+ return;
242
+ }
243
+ const now = Date.now();
244
+ const upsert = rawDb.prepare(`INSERT OR REPLACE INTO recipe_source_refs (recipe_id, source_path, status, verified_at)
245
+ VALUES (?, ?, 'active', ?)`);
246
+ for (const sourcePath of sources) {
247
+ try {
248
+ upsert.run(entryId, sourcePath, now);
249
+ }
250
+ catch {
251
+ /* table may not exist yet */
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * UiStartupTasks — asd ui 启动后异步后台刷新任务
3
+ *
4
+ * 在 Dashboard 启动后异步执行,不阻塞 UI:
5
+ * 1. syncAll: .md → DB 全量同步 + sourceRefs 对账
6
+ * 2. staging promote: 到期 staging → active 晋升
7
+ * 3. vector reconcile: 向量对账(best-effort)
8
+ * 4. refreshIndex: BM25 增量刷新
9
+ */
10
+ interface UiStartupContext {
11
+ projectRoot: string;
12
+ container: {
13
+ get(name: string): unknown;
14
+ services: Record<string, unknown>;
15
+ singletons: Record<string, unknown>;
16
+ };
17
+ }
18
+ export interface UiStartupReport {
19
+ syncAll?: {
20
+ synced: number;
21
+ created: number;
22
+ updated: number;
23
+ };
24
+ reconcile?: {
25
+ inserted: number;
26
+ active: number;
27
+ stale: number;
28
+ };
29
+ staging?: {
30
+ promoted: number;
31
+ };
32
+ vectorReconcile?: {
33
+ orphans: number;
34
+ missing: number;
35
+ };
36
+ indexRefresh?: boolean;
37
+ durationMs: number;
38
+ errors: string[];
39
+ }
40
+ /**
41
+ * 异步执行所有启动后台任务。
42
+ * 每个阶段独立 try/catch,一个失败不影响后续。
43
+ */
44
+ export declare function runUiStartupTasks(ctx: UiStartupContext): Promise<UiStartupReport>;
45
+ export {};
@@ -0,0 +1,101 @@
1
+ /**
2
+ * UiStartupTasks — asd ui 启动后异步后台刷新任务
3
+ *
4
+ * 在 Dashboard 启动后异步执行,不阻塞 UI:
5
+ * 1. syncAll: .md → DB 全量同步 + sourceRefs 对账
6
+ * 2. staging promote: 到期 staging → active 晋升
7
+ * 3. vector reconcile: 向量对账(best-effort)
8
+ * 4. refreshIndex: BM25 增量刷新
9
+ */
10
+ import Logger from '../../infrastructure/logging/Logger.js';
11
+ const logger = Logger.getInstance();
12
+ /**
13
+ * 异步执行所有启动后台任务。
14
+ * 每个阶段独立 try/catch,一个失败不影响后续。
15
+ */
16
+ export async function runUiStartupTasks(ctx) {
17
+ const start = Date.now();
18
+ const report = { durationMs: 0, errors: [] };
19
+ logger.info('[UiStartupTasks] Starting background refresh...');
20
+ // ── Stage 1: syncAll (.md → DB + sourceRefs reconcile) ──
21
+ try {
22
+ const { KnowledgeSyncService } = await import('../../cli/KnowledgeSyncService.js');
23
+ const syncService = new KnowledgeSyncService(ctx.projectRoot);
24
+ const db = ctx.container.get('database');
25
+ const rawDb = db.getDb();
26
+ const syncReport = await syncService.syncAll(rawDb, { skipViolations: true });
27
+ report.syncAll = {
28
+ synced: syncReport.synced,
29
+ created: syncReport.created,
30
+ updated: syncReport.updated,
31
+ };
32
+ if (syncReport.reconcileReport) {
33
+ report.reconcile = {
34
+ inserted: syncReport.reconcileReport.inserted,
35
+ active: syncReport.reconcileReport.active,
36
+ stale: syncReport.reconcileReport.stale,
37
+ };
38
+ }
39
+ logger.info('[UiStartupTasks] Stage 1 complete: syncAll', report.syncAll);
40
+ }
41
+ catch (err) {
42
+ const msg = `syncAll failed: ${err.message}`;
43
+ report.errors.push(msg);
44
+ logger.warn(`[UiStartupTasks] ${msg}`);
45
+ }
46
+ // ── Stage 2: Staging auto-promotion (Bug 2 fix) ──
47
+ try {
48
+ if (ctx.container.services.stagingManager) {
49
+ const sm = ctx.container.get('stagingManager');
50
+ const result = sm.checkAndPromote();
51
+ report.staging = { promoted: result.promoted.length };
52
+ if (result.promoted.length > 0) {
53
+ logger.info(`[UiStartupTasks] Stage 2: auto-promoted ${result.promoted.length} staging entries`);
54
+ }
55
+ }
56
+ }
57
+ catch (err) {
58
+ const msg = `staging promote failed: ${err.message}`;
59
+ report.errors.push(msg);
60
+ logger.warn(`[UiStartupTasks] ${msg}`);
61
+ }
62
+ // ── Stage 3: Vector reconcile (best-effort) ──
63
+ try {
64
+ if (ctx.container.services.vectorService) {
65
+ const vectorService = ctx.container.get('vectorService');
66
+ if (vectorService.syncCoordinator &&
67
+ typeof vectorService.syncCoordinator.reconcile === 'function') {
68
+ const result = await vectorService.syncCoordinator.reconcile();
69
+ report.vectorReconcile = {
70
+ orphans: result.orphansRemoved,
71
+ missing: result.missingQueued,
72
+ };
73
+ logger.info('[UiStartupTasks] Stage 3: vector reconcile complete', report.vectorReconcile);
74
+ }
75
+ }
76
+ }
77
+ catch (err) {
78
+ const msg = `vector reconcile failed: ${err.message}`;
79
+ report.errors.push(msg);
80
+ logger.warn(`[UiStartupTasks] ${msg}`);
81
+ }
82
+ // ── Stage 4: BM25 index refresh ──
83
+ try {
84
+ if (ctx.container.services.searchEngine) {
85
+ const searchEngine = ctx.container.get('searchEngine');
86
+ searchEngine.refreshIndex({ force: true });
87
+ report.indexRefresh = true;
88
+ logger.info('[UiStartupTasks] Stage 4: BM25 index refreshed');
89
+ }
90
+ }
91
+ catch (err) {
92
+ const msg = `index refresh failed: ${err.message}`;
93
+ report.errors.push(msg);
94
+ logger.warn(`[UiStartupTasks] ${msg}`);
95
+ }
96
+ report.durationMs = Date.now() - start;
97
+ logger.info(`[UiStartupTasks] All tasks completed in ${report.durationMs}ms`, {
98
+ errors: report.errors.length,
99
+ });
100
+ return report;
101
+ }
@@ -301,10 +301,10 @@ export class ConsolidationAdvisor {
301
301
  // 同 category 的 Recipe 是最有可能重叠的
302
302
  rows = this.#db
303
303
  .prepare(`SELECT id, title,
304
- do_clause AS doClause,
305
- dont_clause AS dontClause,
304
+ doClause,
305
+ dontClause,
306
306
  json_extract(content, '$.coreCode') AS coreCode,
307
- category, trigger, when_clause AS whenClause,
307
+ category, trigger, whenClause,
308
308
  json_extract(content, '$.pattern') AS guardPattern
309
309
  FROM knowledge_entries
310
310
  WHERE lifecycle IN ('active', 'staging', 'evolving', 'pending')
@@ -316,10 +316,10 @@ export class ConsolidationAdvisor {
316
316
  if (rows.length < 5 && triggerPrefix.length >= 3) {
317
317
  const extra = this.#db
318
318
  .prepare(`SELECT id, title,
319
- do_clause AS doClause,
320
- dont_clause AS dontClause,
319
+ doClause,
320
+ dontClause,
321
321
  json_extract(content, '$.coreCode') AS coreCode,
322
- category, trigger, when_clause AS whenClause,
322
+ category, trigger, whenClause,
323
323
  json_extract(content, '$.pattern') AS guardPattern
324
324
  FROM knowledge_entries
325
325
  WHERE lifecycle IN ('active', 'staging', 'evolving', 'pending')
@@ -339,10 +339,10 @@ export class ConsolidationAdvisor {
339
339
  // 无 category 时按 title 关键词粗筛
340
340
  rows = this.#db
341
341
  .prepare(`SELECT id, title,
342
- do_clause AS doClause,
343
- dont_clause AS dontClause,
342
+ doClause,
343
+ dontClause,
344
344
  json_extract(content, '$.coreCode') AS coreCode,
345
- category, trigger, when_clause AS whenClause,
345
+ category, trigger, whenClause,
346
346
  json_extract(content, '$.pattern') AS guardPattern
347
347
  FROM knowledge_entries
348
348
  WHERE lifecycle IN ('active', 'staging', 'evolving', 'pending')
@@ -160,8 +160,8 @@ export class ContradictionDetector {
160
160
  const rows = this.#db
161
161
  .prepare(`SELECT id, title, lifecycle, description,
162
162
  json_extract(content, '$.markdown') AS content_markdown,
163
- do_clause AS doClause,
164
- dont_clause AS dontClause,
163
+ doClause,
164
+ dontClause,
165
165
  json_extract(content, '$.pattern') AS guardPattern
166
166
  FROM knowledge_entries
167
167
  WHERE lifecycle IN ('active', 'staging', 'evolving')`)
@@ -92,8 +92,8 @@ export class RedundancyAnalyzer {
92
92
  try {
93
93
  const rows = this.#db
94
94
  .prepare(`SELECT id, title,
95
- do_clause AS doClause,
96
- dont_clause AS dontClause,
95
+ doClause,
96
+ dontClause,
97
97
  json_extract(content, '$.pattern') AS guardPattern,
98
98
  json_extract(content, '$.coreCode') AS coreCode
99
99
  FROM knowledge_entries
@@ -0,0 +1,68 @@
1
+ /**
2
+ * SourceRefReconciler — Recipe 来源引用健康检查 + 自动修复
3
+ *
4
+ * 从 knowledge_entries.reasoning.sources 填充 recipe_source_refs 桥接表,
5
+ * 验证路径存在性,检测 git rename,修复路径引用。
6
+ *
7
+ * 状态机:
8
+ * active — 文件存在,路径有效
9
+ * renamed — 文件已移动到 new_path,等待修复
10
+ * stale — 路径失效,无法自动修复
11
+ */
12
+ interface DatabaseLike {
13
+ prepare(sql: string): {
14
+ all(...params: unknown[]): Record<string, unknown>[];
15
+ get(...params: unknown[]): Record<string, unknown> | undefined;
16
+ run(...params: unknown[]): {
17
+ changes: number;
18
+ };
19
+ };
20
+ }
21
+ export interface ReconcileReport {
22
+ /** 新插入的 sourceRef 条目 */
23
+ inserted: number;
24
+ /** 验证为 active 的条目 */
25
+ active: number;
26
+ /** 标记为 stale 的条目 */
27
+ stale: number;
28
+ /** 跳过的条目(24h 内已验证) */
29
+ skipped: number;
30
+ /** 处理的 recipe 数 */
31
+ recipesProcessed: number;
32
+ }
33
+ export interface RepairReport {
34
+ /** 成功检测到 rename 的条目 */
35
+ renamed: number;
36
+ /** 仍然 stale 的条目 */
37
+ stillStale: number;
38
+ }
39
+ export interface ApplyReport {
40
+ /** 成功写回 .md 的条目 */
41
+ applied: number;
42
+ /** 写回失败的条目 */
43
+ failed: number;
44
+ }
45
+ export declare class SourceRefReconciler {
46
+ #private;
47
+ constructor(projectRoot: string, db: DatabaseLike, options?: {
48
+ ttlMs?: number;
49
+ });
50
+ /**
51
+ * 从 knowledge_entries.reasoning 填充 recipe_source_refs 表。
52
+ * 对已有条目验证路径存在性,更新 status。
53
+ */
54
+ reconcile(opts?: {
55
+ force?: boolean;
56
+ }): ReconcileReport;
57
+ /**
58
+ * 对 stale 条目尝试 git rename 修复。
59
+ * 使用 execFile() 安全执行 git log(防止命令注入)。
60
+ */
61
+ repairRenames(): Promise<RepairReport>;
62
+ /**
63
+ * 将 renamed 条目的 new_path 写回 Recipe .md 文件的 _reasoning.sources。
64
+ * 完成后 status → active。
65
+ */
66
+ applyRepairs(): ApplyReport;
67
+ }
68
+ export {};
@@ -0,0 +1,309 @@
1
+ /**
2
+ * SourceRefReconciler — Recipe 来源引用健康检查 + 自动修复
3
+ *
4
+ * 从 knowledge_entries.reasoning.sources 填充 recipe_source_refs 桥接表,
5
+ * 验证路径存在性,检测 git rename,修复路径引用。
6
+ *
7
+ * 状态机:
8
+ * active — 文件存在,路径有效
9
+ * renamed — 文件已移动到 new_path,等待修复
10
+ * stale — 路径失效,无法自动修复
11
+ */
12
+ import { execFile } from 'node:child_process';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { promisify } from 'node:util';
16
+ import Logger from '../../infrastructure/logging/Logger.js';
17
+ const execFileAsync = promisify(execFile);
18
+ /* ────────────────────── Class ────────────────────── */
19
+ /** 默认跳过 24h 内已验证的条目 */
20
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
21
+ export class SourceRefReconciler {
22
+ #projectRoot;
23
+ #db;
24
+ #logger = Logger.getInstance();
25
+ #ttlMs;
26
+ constructor(projectRoot, db, options) {
27
+ this.#projectRoot = projectRoot;
28
+ this.#db = db;
29
+ this.#ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
30
+ }
31
+ /**
32
+ * 从 knowledge_entries.reasoning 填充 recipe_source_refs 表。
33
+ * 对已有条目验证路径存在性,更新 status。
34
+ */
35
+ reconcile(opts) {
36
+ const force = opts?.force ?? false;
37
+ const report = {
38
+ inserted: 0,
39
+ active: 0,
40
+ stale: 0,
41
+ skipped: 0,
42
+ recipesProcessed: 0,
43
+ };
44
+ // 确保表存在(兼容未跑 migration 的场景)
45
+ this.#ensureTable();
46
+ // 获取所有有 reasoning 的知识条目
47
+ const rows = this.#db
48
+ .prepare(`SELECT id, reasoning FROM knowledge_entries WHERE reasoning IS NOT NULL AND reasoning != '{}'`)
49
+ .all();
50
+ const now = Date.now();
51
+ for (const row of rows) {
52
+ let sources = [];
53
+ try {
54
+ const reasoning = JSON.parse(row.reasoning);
55
+ sources = Array.isArray(reasoning.sources)
56
+ ? reasoning.sources.filter((s) => typeof s === 'string' && s.length > 0)
57
+ : [];
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ if (sources.length === 0) {
63
+ continue;
64
+ }
65
+ report.recipesProcessed++;
66
+ for (const sourcePath of sources) {
67
+ // 检查是否已有记录
68
+ const existing = this.#db
69
+ .prepare(`SELECT status, verified_at FROM recipe_source_refs WHERE recipe_id = ? AND source_path = ?`)
70
+ .get(row.id, sourcePath);
71
+ if (existing && !force) {
72
+ // TTL 检查:跳过近期已验证的条目
73
+ if (now - existing.verified_at < this.#ttlMs) {
74
+ report.skipped++;
75
+ if (existing.status === 'active') {
76
+ report.active++;
77
+ }
78
+ else if (existing.status === 'stale') {
79
+ report.stale++;
80
+ }
81
+ continue;
82
+ }
83
+ }
84
+ // 验证路径存在性
85
+ const absPath = path.resolve(this.#projectRoot, sourcePath);
86
+ const exists = fs.existsSync(absPath);
87
+ if (existing) {
88
+ // 更新已有记录
89
+ if (exists) {
90
+ this.#db
91
+ .prepare(`UPDATE recipe_source_refs SET status = 'active', new_path = NULL, verified_at = ? WHERE recipe_id = ? AND source_path = ?`)
92
+ .run(now, row.id, sourcePath);
93
+ report.active++;
94
+ }
95
+ else {
96
+ this.#db
97
+ .prepare(`UPDATE recipe_source_refs SET status = 'stale', verified_at = ? WHERE recipe_id = ? AND source_path = ?`)
98
+ .run(now, row.id, sourcePath);
99
+ report.stale++;
100
+ }
101
+ }
102
+ else {
103
+ // 新增记录
104
+ const status = exists ? 'active' : 'stale';
105
+ this.#db
106
+ .prepare(`INSERT OR REPLACE INTO recipe_source_refs (recipe_id, source_path, status, verified_at) VALUES (?, ?, ?, ?)`)
107
+ .run(row.id, sourcePath, status, now);
108
+ report.inserted++;
109
+ if (exists) {
110
+ report.active++;
111
+ }
112
+ else {
113
+ report.stale++;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ this.#logger.info('SourceRefReconciler: reconcile complete', {
119
+ inserted: report.inserted,
120
+ active: report.active,
121
+ stale: report.stale,
122
+ skipped: report.skipped,
123
+ recipesProcessed: report.recipesProcessed,
124
+ });
125
+ return report;
126
+ }
127
+ /**
128
+ * 对 stale 条目尝试 git rename 修复。
129
+ * 使用 execFile() 安全执行 git log(防止命令注入)。
130
+ */
131
+ async repairRenames() {
132
+ const report = { renamed: 0, stillStale: 0 };
133
+ // 获取所有 stale 条目
134
+ const staleRows = this.#db
135
+ .prepare(`SELECT recipe_id, source_path FROM recipe_source_refs WHERE status = 'stale'`)
136
+ .all();
137
+ if (staleRows.length === 0) {
138
+ return report;
139
+ }
140
+ // 获取 git rename 映射
141
+ const renameMap = await this.#getGitRenameMap();
142
+ const now = Date.now();
143
+ for (const row of staleRows) {
144
+ const newPath = renameMap.get(row.source_path);
145
+ if (newPath) {
146
+ // 验证 newPath 存在
147
+ const absNewPath = path.resolve(this.#projectRoot, newPath);
148
+ if (fs.existsSync(absNewPath)) {
149
+ this.#db
150
+ .prepare(`UPDATE recipe_source_refs SET status = 'renamed', new_path = ?, verified_at = ? WHERE recipe_id = ? AND source_path = ?`)
151
+ .run(newPath, now, row.recipe_id, row.source_path);
152
+ report.renamed++;
153
+ continue;
154
+ }
155
+ }
156
+ report.stillStale++;
157
+ }
158
+ if (report.renamed > 0) {
159
+ this.#logger.info('SourceRefReconciler: rename repair complete', {
160
+ renamed: report.renamed,
161
+ stillStale: report.stillStale,
162
+ });
163
+ }
164
+ return report;
165
+ }
166
+ /**
167
+ * 将 renamed 条目的 new_path 写回 Recipe .md 文件的 _reasoning.sources。
168
+ * 完成后 status → active。
169
+ */
170
+ applyRepairs() {
171
+ const report = { applied: 0, failed: 0 };
172
+ const renamedRows = this.#db
173
+ .prepare(`SELECT recipe_id, source_path, new_path FROM recipe_source_refs WHERE status = 'renamed' AND new_path IS NOT NULL`)
174
+ .all();
175
+ if (renamedRows.length === 0) {
176
+ return report;
177
+ }
178
+ // 按 recipe_id 分组
179
+ const byRecipe = new Map();
180
+ for (const row of renamedRows) {
181
+ if (!byRecipe.has(row.recipe_id)) {
182
+ byRecipe.set(row.recipe_id, []);
183
+ }
184
+ byRecipe.get(row.recipe_id)?.push({ source_path: row.source_path, new_path: row.new_path });
185
+ }
186
+ // 获取 recipe 的 sourceFile 以定位 .md 文件
187
+ const now = Date.now();
188
+ for (const [recipeId, renames] of byRecipe) {
189
+ try {
190
+ const entry = this.#db
191
+ .prepare(`SELECT sourceFile, reasoning FROM knowledge_entries WHERE id = ?`)
192
+ .get(recipeId);
193
+ if (!entry?.sourceFile || !entry.reasoning) {
194
+ report.failed += renames.length;
195
+ continue;
196
+ }
197
+ const mdPath = path.resolve(this.#projectRoot, entry.sourceFile);
198
+ if (!fs.existsSync(mdPath)) {
199
+ report.failed += renames.length;
200
+ continue;
201
+ }
202
+ // 读取并修改 .md 文件中的 reasoning.sources
203
+ const _content = fs.readFileSync(mdPath, 'utf8');
204
+ let reasoning;
205
+ try {
206
+ reasoning = JSON.parse(entry.reasoning);
207
+ }
208
+ catch {
209
+ report.failed += renames.length;
210
+ continue;
211
+ }
212
+ const sources = Array.isArray(reasoning.sources) ? [...reasoning.sources] : [];
213
+ let modified = false;
214
+ for (const rename of renames) {
215
+ const idx = sources.indexOf(rename.source_path);
216
+ if (idx >= 0) {
217
+ sources[idx] = rename.new_path;
218
+ modified = true;
219
+ }
220
+ }
221
+ if (modified) {
222
+ reasoning.sources = sources;
223
+ // 更新 .md 文件中的 reasoning frontmatter
224
+ // 查找 YAML frontmatter 中的 reasoning 并替换
225
+ const updatedReasoning = JSON.stringify(reasoning);
226
+ // 更新 DB reasoning 列
227
+ this.#db
228
+ .prepare(`UPDATE knowledge_entries SET reasoning = ?, updatedAt = ? WHERE id = ?`)
229
+ .run(updatedReasoning, now, recipeId);
230
+ // 更新 recipe_source_refs 状态
231
+ for (const rename of renames) {
232
+ this.#db
233
+ .prepare(`UPDATE recipe_source_refs SET status = 'active', source_path = ?, new_path = NULL, verified_at = ? WHERE recipe_id = ? AND source_path = ?`)
234
+ .run(rename.new_path, now, recipeId, rename.source_path);
235
+ }
236
+ report.applied += renames.length;
237
+ }
238
+ else {
239
+ report.failed += renames.length;
240
+ }
241
+ }
242
+ catch (err) {
243
+ this.#logger.warn('SourceRefReconciler: applyRepairs failed for recipe', {
244
+ recipeId,
245
+ error: err.message,
246
+ });
247
+ report.failed += renames.length;
248
+ }
249
+ }
250
+ if (report.applied > 0) {
251
+ this.#logger.info('SourceRefReconciler: applyRepairs complete', report);
252
+ }
253
+ return report;
254
+ }
255
+ /* ═══ Private helpers ═══════════════════════════════ */
256
+ #ensureTable() {
257
+ try {
258
+ this.#db.prepare(`SELECT 1 FROM recipe_source_refs LIMIT 1`).get();
259
+ }
260
+ catch {
261
+ // 表不存在,创建之
262
+ this.#db.exec?.(`CREATE TABLE IF NOT EXISTS recipe_source_refs (
263
+ recipe_id TEXT NOT NULL,
264
+ source_path TEXT NOT NULL,
265
+ status TEXT NOT NULL DEFAULT 'active',
266
+ new_path TEXT,
267
+ verified_at INTEGER NOT NULL,
268
+ PRIMARY KEY (recipe_id, source_path),
269
+ FOREIGN KEY (recipe_id) REFERENCES knowledge_entries(id) ON DELETE CASCADE
270
+ );
271
+ CREATE INDEX IF NOT EXISTS idx_rsr_path ON recipe_source_refs(source_path);
272
+ CREATE INDEX IF NOT EXISTS idx_rsr_status ON recipe_source_refs(status);`);
273
+ }
274
+ }
275
+ /**
276
+ * 通过 git log 获取 rename 映射(旧路径 → 新路径)
277
+ * 使用 execFile 防止命令注入
278
+ */
279
+ async #getGitRenameMap() {
280
+ const renameMap = new Map();
281
+ try {
282
+ const { stdout } = await execFileAsync('git', ['log', '--diff-filter=R', '--name-status', '--pretty=format:', '-n', '200'], {
283
+ cwd: this.#projectRoot,
284
+ timeout: 10000,
285
+ maxBuffer: 1024 * 1024,
286
+ });
287
+ // 解析 git log 输出: R100\told_path\tnew_path
288
+ for (const line of stdout.split('\n')) {
289
+ const trimmed = line.trim();
290
+ if (!trimmed.startsWith('R')) {
291
+ continue;
292
+ }
293
+ const parts = trimmed.split('\t');
294
+ if (parts.length >= 3) {
295
+ const oldPath = parts[1];
296
+ const newPath = parts[2];
297
+ if (oldPath && newPath) {
298
+ renameMap.set(oldPath, newPath);
299
+ }
300
+ }
301
+ }
302
+ }
303
+ catch {
304
+ // git 不可用或不在 git 仓库中 — 跳过 rename 检测
305
+ this.#logger.debug('SourceRefReconciler: git rename detection unavailable');
306
+ }
307
+ return renameMap;
308
+ }
309
+ }