autosnippet 3.3.4 → 3.3.5

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 (29) hide show
  1. package/README.md +174 -83
  2. package/config/constitution.yaml +2 -0
  3. package/dist/lib/cli/KnowledgeSyncService.d.ts +5 -1
  4. package/dist/lib/cli/KnowledgeSyncService.js +5 -2
  5. package/dist/lib/domain/knowledge/values/Stats.d.ts +1 -1
  6. package/dist/lib/domain/knowledge/values/Stats.js +2 -2
  7. package/dist/lib/external/mcp/handlers/consolidated.js +178 -0
  8. package/dist/lib/external/mcp/tools.js +2 -1
  9. package/dist/lib/injection/modules/InfraModule.js +4 -1
  10. package/dist/lib/injection/modules/KnowledgeModule.js +23 -0
  11. package/dist/lib/repository/evolution/ProposalRepository.d.ts +99 -0
  12. package/dist/lib/repository/evolution/ProposalRepository.js +255 -0
  13. package/dist/lib/service/bootstrap/UiStartupTasks.d.ts +17 -4
  14. package/dist/lib/service/bootstrap/UiStartupTasks.js +53 -5
  15. package/dist/lib/service/evolution/DecayDetector.d.ts +4 -3
  16. package/dist/lib/service/evolution/DecayDetector.js +97 -22
  17. package/dist/lib/service/evolution/KnowledgeMetabolism.d.ts +4 -2
  18. package/dist/lib/service/evolution/KnowledgeMetabolism.js +29 -2
  19. package/dist/lib/service/evolution/ProposalExecutor.d.ts +62 -0
  20. package/dist/lib/service/evolution/ProposalExecutor.js +360 -0
  21. package/dist/lib/service/evolution/StagingManager.js +5 -3
  22. package/dist/lib/service/guard/GuardCrossFileChecks.js +2 -0
  23. package/dist/lib/service/guard/ReverseGuard.d.ts +1 -1
  24. package/dist/lib/service/guard/ReverseGuard.js +32 -2
  25. package/dist/lib/service/knowledge/SourceRefReconciler.d.ts +2 -0
  26. package/dist/lib/service/knowledge/SourceRefReconciler.js +48 -0
  27. package/dist/lib/shared/schemas/mcp-tools.d.ts +1 -0
  28. package/dist/lib/shared/schemas/mcp-tools.js +4 -0
  29. package/package.json +1 -1
@@ -0,0 +1,360 @@
1
+ /**
2
+ * ProposalExecutor — 到期自动执行引擎
3
+ *
4
+ * 核心职责:
5
+ * 1. 扫描所有 observing 状态的 Proposal,检查是否到期
6
+ * 2. 到期 → 收集观察期表现数据 → 评估执行判据
7
+ * 3. 通过 → 执行操作(merge/deprecate/enhance/...)
8
+ * 4. 不通过 → 拒绝 Proposal,Recipe 恢复原状态
9
+ *
10
+ * 触发时机:UiStartupTasks Stage 5
11
+ *
12
+ * 安全边界:
13
+ * - Agent 只做分析,ProposalExecutor 做执行
14
+ * - merge/enhance 执行后 Recipe → staging(走正常路径)
15
+ * - contradiction/reorganize 始终等开发者确认(不自动执行)
16
+ * - 到期无判据 → expired
17
+ */
18
+ import Logger from '../../infrastructure/logging/Logger.js';
19
+ /* ────────────────────── Constants ────────────────────── */
20
+ /** 高风险类型:需开发者确认,不自动执行 */
21
+ const HIGH_RISK_TYPES = new Set(['contradiction', 'reorganize']);
22
+ /** 超过此天数未操作的 pending Proposal 自动过期 */
23
+ const PENDING_EXPIRY_DAYS = 14;
24
+ /* ────────────────────── Class ────────────────────── */
25
+ export class ProposalExecutor {
26
+ #db;
27
+ #repo;
28
+ #signalBus;
29
+ #logger = Logger.getInstance();
30
+ constructor(db, repo, options = {}) {
31
+ this.#db = db;
32
+ this.#repo = repo;
33
+ this.#signalBus = options.signalBus ?? null;
34
+ }
35
+ /**
36
+ * 定期调用(UiStartupTasks Stage 5)
37
+ *
38
+ * 扫描所有到期 Proposal → 评估 → 执行/拒绝/过期
39
+ */
40
+ checkAndExecute() {
41
+ const result = {
42
+ executed: [],
43
+ rejected: [],
44
+ expired: [],
45
+ skipped: [],
46
+ };
47
+ // 1. 处理到期的 observing Proposal
48
+ const expiredObserving = this.#repo.findExpiredObserving();
49
+ for (const proposal of expiredObserving) {
50
+ if (HIGH_RISK_TYPES.has(proposal.type)) {
51
+ // 高风险类型跳过自动执行
52
+ result.skipped.push({
53
+ id: proposal.id,
54
+ type: proposal.type,
55
+ reason: 'high-risk type requires developer confirmation',
56
+ });
57
+ continue;
58
+ }
59
+ this.#processExpiredProposal(proposal, result);
60
+ }
61
+ // 2. 清理超期未操作的 pending Proposal
62
+ this.#expireOldPending(result);
63
+ if (result.executed.length > 0 || result.rejected.length > 0 || result.expired.length > 0) {
64
+ this.#logger.info(`[ProposalExecutor] checkAndExecute complete: ` +
65
+ `executed=${result.executed.length}, rejected=${result.rejected.length}, expired=${result.expired.length}`);
66
+ }
67
+ return result;
68
+ }
69
+ /* ═══════════════════ Internal ═══════════════════ */
70
+ #processExpiredProposal(proposal, result) {
71
+ const metrics = this.#collectRecipeMetrics(proposal.targetRecipeId);
72
+ const snapshot = this.#extractSnapshot(proposal);
73
+ switch (proposal.type) {
74
+ case 'merge':
75
+ case 'enhance':
76
+ this.#executeMergeOrEnhance(proposal, metrics, snapshot, result);
77
+ break;
78
+ case 'supersede':
79
+ this.#executeSupersede(proposal, metrics, snapshot, result);
80
+ break;
81
+ case 'deprecate':
82
+ this.#executeDeprecate(proposal, metrics, snapshot, result);
83
+ break;
84
+ case 'correction':
85
+ this.#executeCorrection(proposal, metrics, result);
86
+ break;
87
+ default:
88
+ result.skipped.push({
89
+ id: proposal.id,
90
+ type: proposal.type,
91
+ reason: `unhandled type: ${proposal.type}`,
92
+ });
93
+ }
94
+ }
95
+ /* ── merge / enhance ── */
96
+ #executeMergeOrEnhance(proposal, metrics, snapshot, result) {
97
+ // 执行判据:
98
+ // - 目标 Recipe 在观察期内无 FP rate 异常飙升
99
+ // - 目标 Recipe 在观察期内仍有使用
100
+ const fpOk = metrics.ruleFalsePositiveRate < 0.4;
101
+ const hasUsage = metrics.guardHits > 0 || metrics.searchHits > 0;
102
+ if (fpOk && hasUsage) {
103
+ // 通过 → 将 Recipe 回到 staging(重新走 ConfidenceRouter)
104
+ this.#transitionRecipe(proposal.targetRecipeId, 'staging');
105
+ this.#repo.markExecuted(proposal.id, `观察期表现合格: FP=${(metrics.ruleFalsePositiveRate * 100).toFixed(0)}%, hits=${metrics.guardHits + metrics.searchHits}`);
106
+ result.executed.push({
107
+ id: proposal.id,
108
+ type: proposal.type,
109
+ targetRecipeId: proposal.targetRecipeId,
110
+ });
111
+ this.#emitSignal(proposal, 'executed');
112
+ }
113
+ else {
114
+ // 不通过 → Recipe 恢复原状态
115
+ this.#restoreRecipe(proposal.targetRecipeId);
116
+ this.#repo.markRejected(proposal.id, `观察期表现不达标: FP=${(metrics.ruleFalsePositiveRate * 100).toFixed(0)}%, hasUsage=${hasUsage}`);
117
+ result.rejected.push({
118
+ id: proposal.id,
119
+ type: proposal.type,
120
+ reason: fpOk ? 'no usage during observation' : 'FP rate too high',
121
+ });
122
+ this.#emitSignal(proposal, 'rejected');
123
+ }
124
+ }
125
+ /* ── supersede ── */
126
+ #executeSupersede(proposal, metrics, snapshot, result) {
127
+ // 新 Recipe 必须已到达 active
128
+ const newRecipeId = proposal.relatedRecipeIds[0];
129
+ if (!newRecipeId) {
130
+ this.#repo.markRejected(proposal.id, 'no related new recipe specified');
131
+ result.rejected.push({
132
+ id: proposal.id,
133
+ type: proposal.type,
134
+ reason: 'no related new recipe',
135
+ });
136
+ return;
137
+ }
138
+ const newRecipe = this.#getRecipeLifecycle(newRecipeId);
139
+ if (newRecipe?.lifecycle !== 'active') {
140
+ // 新 Recipe 尚未 active → 跳过,等下次检查
141
+ result.skipped.push({
142
+ id: proposal.id,
143
+ type: proposal.type,
144
+ reason: `new recipe ${newRecipeId} not yet active (lifecycle: ${newRecipe?.lifecycle ?? 'unknown'})`,
145
+ });
146
+ return;
147
+ }
148
+ // 对比新旧 Recipe 的使用数据
149
+ const newMetrics = this.#collectRecipeMetrics(newRecipeId);
150
+ const oldUsage = metrics.guardHits + metrics.searchHits;
151
+ const newUsage = newMetrics.guardHits + newMetrics.searchHits;
152
+ if (newUsage >= oldUsage * 0.5 || oldUsage === 0) {
153
+ // 新 Recipe 表现足够 → 旧 Recipe → decaying,建立 deprecated_by
154
+ this.#transitionRecipe(proposal.targetRecipeId, 'decaying');
155
+ this.#createDeprecatedByEdge(newRecipeId, proposal.targetRecipeId);
156
+ this.#repo.markExecuted(proposal.id, `supersede executed: new usage=${newUsage}, old usage=${oldUsage}`);
157
+ result.executed.push({
158
+ id: proposal.id,
159
+ type: proposal.type,
160
+ targetRecipeId: proposal.targetRecipeId,
161
+ });
162
+ this.#emitSignal(proposal, 'executed');
163
+ }
164
+ else {
165
+ // 新 Recipe 表现不足 → 拒绝
166
+ this.#restoreRecipe(proposal.targetRecipeId);
167
+ this.#repo.markRejected(proposal.id, `new recipe usage (${newUsage}) < 50% of old (${oldUsage})`);
168
+ result.rejected.push({
169
+ id: proposal.id,
170
+ type: proposal.type,
171
+ reason: 'new recipe insufficient usage',
172
+ });
173
+ this.#emitSignal(proposal, 'rejected');
174
+ }
175
+ }
176
+ /* ── deprecate ── */
177
+ #executeDeprecate(proposal, metrics, snapshot, result) {
178
+ const currentDecay = metrics.decayScore;
179
+ const snapshotDecay = snapshot?.decayScore ?? currentDecay;
180
+ // 观察期内 decayScore 有回升 → 拒绝
181
+ if (currentDecay > snapshotDecay + 10) {
182
+ this.#restoreRecipe(proposal.targetRecipeId);
183
+ this.#repo.markRejected(proposal.id, `decayScore recovered: ${snapshotDecay} → ${currentDecay}`);
184
+ result.rejected.push({
185
+ id: proposal.id,
186
+ type: proposal.type,
187
+ reason: 'decay score recovered during observation',
188
+ });
189
+ this.#emitSignal(proposal, 'rejected');
190
+ return;
191
+ }
192
+ // 无回升 → 根据 decayScore 决定操作
193
+ if (currentDecay <= 19) {
194
+ // 死亡 → 直接 deprecated
195
+ this.#transitionRecipe(proposal.targetRecipeId, 'deprecated');
196
+ this.#repo.markExecuted(proposal.id, `deprecated (dead): decayScore=${currentDecay}`);
197
+ }
198
+ else if (currentDecay <= 40) {
199
+ // 严重 → decaying
200
+ this.#transitionRecipe(proposal.targetRecipeId, 'decaying');
201
+ this.#repo.markExecuted(proposal.id, `decaying (severe): decayScore=${currentDecay}`);
202
+ }
203
+ else {
204
+ // 衰退减缓 → 拒绝
205
+ this.#restoreRecipe(proposal.targetRecipeId);
206
+ this.#repo.markRejected(proposal.id, `decayScore above threshold (${currentDecay}), not critical enough`);
207
+ result.rejected.push({
208
+ id: proposal.id,
209
+ type: proposal.type,
210
+ reason: `decayScore (${currentDecay}) not critical`,
211
+ });
212
+ this.#emitSignal(proposal, 'rejected');
213
+ return;
214
+ }
215
+ result.executed.push({
216
+ id: proposal.id,
217
+ type: proposal.type,
218
+ targetRecipeId: proposal.targetRecipeId,
219
+ });
220
+ this.#emitSignal(proposal, 'executed');
221
+ }
222
+ /* ── correction ── */
223
+ #executeCorrection(proposal, metrics, result) {
224
+ // correction 低风险,到期直接执行(Recipe → staging 重新审核)
225
+ const hasUsage = metrics.guardHits > 0 || metrics.searchHits > 0;
226
+ if (hasUsage) {
227
+ this.#transitionRecipe(proposal.targetRecipeId, 'staging');
228
+ this.#repo.markExecuted(proposal.id, 'correction applied, recipe → staging for re-review');
229
+ result.executed.push({
230
+ id: proposal.id,
231
+ type: proposal.type,
232
+ targetRecipeId: proposal.targetRecipeId,
233
+ });
234
+ this.#emitSignal(proposal, 'executed');
235
+ }
236
+ else {
237
+ this.#repo.markRejected(proposal.id, 'no usage during observation, correction unnecessary');
238
+ result.rejected.push({
239
+ id: proposal.id,
240
+ type: proposal.type,
241
+ reason: 'no usage',
242
+ });
243
+ this.#emitSignal(proposal, 'rejected');
244
+ }
245
+ }
246
+ /* ── expired pending cleanup ── */
247
+ #expireOldPending(result) {
248
+ const cutoff = Date.now() - PENDING_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
249
+ const oldPending = this.#repo.find({
250
+ status: 'pending',
251
+ expiredBefore: cutoff,
252
+ });
253
+ for (const proposal of oldPending) {
254
+ this.#repo.markExpired(proposal.id);
255
+ result.expired.push({
256
+ id: proposal.id,
257
+ type: proposal.type,
258
+ });
259
+ }
260
+ }
261
+ /* ═══════════════════ DB Helpers ═══════════════════ */
262
+ #collectRecipeMetrics(recipeId) {
263
+ const row = this.#db
264
+ .prepare(`SELECT stats, quality FROM knowledge_entries WHERE id = ?`)
265
+ .get(recipeId);
266
+ if (!row) {
267
+ return {
268
+ guardHits: 0,
269
+ searchHits: 0,
270
+ hitsLast30d: 0,
271
+ decayScore: 0,
272
+ ruleFalsePositiveRate: 0,
273
+ quality: 0,
274
+ };
275
+ }
276
+ const stats = safeJsonParse(row.stats, {});
277
+ const quality = safeJsonParse(row.quality, {});
278
+ return {
279
+ guardHits: stats.guardHits ?? 0,
280
+ searchHits: stats.searchHits ?? 0,
281
+ hitsLast30d: stats.hitsLast30d ?? 0,
282
+ decayScore: stats.decayScore ?? 50,
283
+ ruleFalsePositiveRate: stats.ruleFalsePositiveRate ?? 0,
284
+ quality: quality.overall ?? 0,
285
+ };
286
+ }
287
+ #extractSnapshot(proposal) {
288
+ for (const ev of proposal.evidence) {
289
+ if (ev.snapshotAt && ev.metrics) {
290
+ const m = ev.metrics;
291
+ return {
292
+ guardHits: m.guardHits ?? 0,
293
+ searchHits: m.searchHits ?? 0,
294
+ hitsLast30d: m.hitsLast30d ?? 0,
295
+ decayScore: m.decayScore ?? 50,
296
+ ruleFalsePositiveRate: m.ruleFalsePositiveRate ?? 0,
297
+ quality: m.quality?.overall ?? 0,
298
+ };
299
+ }
300
+ }
301
+ return null;
302
+ }
303
+ #getRecipeLifecycle(recipeId) {
304
+ const row = this.#db
305
+ .prepare(`SELECT lifecycle FROM knowledge_entries WHERE id = ?`)
306
+ .get(recipeId);
307
+ return row ?? null;
308
+ }
309
+ #transitionRecipe(recipeId, newLifecycle) {
310
+ this.#db
311
+ .prepare(`UPDATE knowledge_entries SET lifecycle = ?, updatedAt = ? WHERE id = ?`)
312
+ .run(newLifecycle, Date.now(), recipeId);
313
+ }
314
+ #restoreRecipe(recipeId) {
315
+ // 恢复到 active(evolving/decaying → active)
316
+ const current = this.#getRecipeLifecycle(recipeId);
317
+ if (current && (current.lifecycle === 'evolving' || current.lifecycle === 'decaying')) {
318
+ this.#transitionRecipe(recipeId, 'active');
319
+ }
320
+ }
321
+ #createDeprecatedByEdge(newRecipeId, oldRecipeId) {
322
+ const now = Date.now();
323
+ try {
324
+ this.#db
325
+ .prepare(`INSERT OR IGNORE INTO knowledge_edges (from_id, from_type, to_id, to_type, relation, weight, metadata_json, created_at, updated_at)
326
+ VALUES (?, 'recipe', ?, 'recipe', 'deprecated_by', 1.0, '{}', ?, ?)`)
327
+ .run(newRecipeId, oldRecipeId, now, now);
328
+ }
329
+ catch {
330
+ // knowledge_edges 表可能不存在(降级容忍)
331
+ }
332
+ }
333
+ /* ═══════════════════ Signal ═══════════════════ */
334
+ #emitSignal(proposal, action) {
335
+ if (!this.#signalBus) {
336
+ return;
337
+ }
338
+ this.#signalBus.send('lifecycle', 'ProposalExecutor', proposal.confidence, {
339
+ target: proposal.targetRecipeId,
340
+ metadata: {
341
+ proposalId: proposal.id,
342
+ proposalType: proposal.type,
343
+ action,
344
+ source: proposal.source,
345
+ },
346
+ });
347
+ }
348
+ }
349
+ /* ────────────────────── Util ────────────────────── */
350
+ function safeJsonParse(json, fallback) {
351
+ if (!json) {
352
+ return fallback;
353
+ }
354
+ try {
355
+ return JSON.parse(json);
356
+ }
357
+ catch {
358
+ return fallback;
359
+ }
360
+ }
@@ -12,6 +12,7 @@
12
12
  * 0.85-0.89 → 72h
13
13
  */
14
14
  import Logger from '../../infrastructure/logging/Logger.js';
15
+ import { unixNow } from '../../shared/utils/common.js';
15
16
  /* ────────────────────── Class ────────────────────── */
16
17
  export class StagingManager {
17
18
  #db;
@@ -54,7 +55,7 @@ export class StagingManager {
54
55
  stats.stagingEnteredAt = now;
55
56
  this.#db
56
57
  .prepare(`UPDATE knowledge_entries SET lifecycle = 'staging', stats = ?, updatedAt = ? WHERE id = ?`)
57
- .run(JSON.stringify(stats), now, entryId);
58
+ .run(JSON.stringify(stats), unixNow(), entryId);
58
59
  // 发射信号
59
60
  if (this.#signalBus) {
60
61
  this.#signalBus.send('lifecycle', 'StagingManager.enter', confidence, {
@@ -140,7 +141,7 @@ export class StagingManager {
140
141
  stats.lastRollbackAt = now;
141
142
  this.#db
142
143
  .prepare(`UPDATE knowledge_entries SET lifecycle = 'pending', stats = ?, updatedAt = ? WHERE id = ?`)
143
- .run(JSON.stringify(stats), now, entryId);
144
+ .run(JSON.stringify(stats), unixNow(), entryId);
144
145
  if (this.#signalBus) {
145
146
  this.#signalBus.send('lifecycle', 'StagingManager.rollback', 0.8, {
146
147
  target: entryId,
@@ -184,9 +185,10 @@ export class StagingManager {
184
185
  delete stats.stagingConfidence;
185
186
  delete stats.stagingEnteredAt;
186
187
  stats.autoPublishedAt = now;
188
+ const nowS = unixNow();
187
189
  this.#db
188
190
  .prepare(`UPDATE knowledge_entries SET lifecycle = 'active', publishedAt = ?, stats = ?, updatedAt = ? WHERE id = ?`)
189
- .run(now, JSON.stringify(stats), now, entry.id);
191
+ .run(nowS, JSON.stringify(stats), nowS, entry.id);
190
192
  if (this.#signalBus) {
191
193
  this.#signalBus.send('lifecycle', 'StagingManager.promote', 1.0, {
192
194
  target: entry.id,
@@ -42,6 +42,8 @@ export function runCrossFileChecks(files, options = {}) {
42
42
  const violations = [];
43
43
  const disabledSet = new Set(options.disabledRules || []);
44
44
  const isDisabled = (ruleId) => disabledSet.has(ruleId);
45
+ // 过滤掉 content 为空的条目,防止下游 split 崩溃
46
+ files = files.filter((f) => typeof f.content === 'string');
45
47
  // ── ObjC Category 跨文件重名检查 ──
46
48
  if (!isDisabled('objc-cross-file-duplicate-category')) {
47
49
  const categoryMap = new Map();
@@ -18,7 +18,7 @@ interface DatabaseLike {
18
18
  get(...params: unknown[]): Record<string, unknown> | undefined;
19
19
  };
20
20
  }
21
- export type DriftType = 'symbol_missing' | 'match_rate_drop' | 'api_deprecated' | 'zero_match';
21
+ export type DriftType = 'symbol_missing' | 'match_rate_drop' | 'api_deprecated' | 'zero_match' | 'source_ref_stale';
22
22
  export type DriftSeverity = 'high' | 'medium' | 'low';
23
23
  export interface PatternDriftSignal {
24
24
  type: DriftType;
@@ -59,9 +59,11 @@ export class ReverseGuard {
59
59
  if (recipe.guard_pattern) {
60
60
  signals.push(...this.#checkPatternMatchRate(recipe.id, recipe.guard_pattern, projectFiles));
61
61
  }
62
- // 3. 综合判定
62
+ // 3. 检查 sourceRef 路径是否失效(与 SourceRefReconciler 数据交叉验证)
63
+ signals.push(...this.#checkSourceRefStaleness(recipe.id));
64
+ // 4. 综合判定
63
65
  const recommendation = this.#computeRecommendation(signals);
64
- // 4. 发射信号
66
+ // 5. 发射信号
65
67
  if (this.#signalBus && signals.length > 0) {
66
68
  const severity = recommendation === 'decay' ? 1 : recommendation === 'investigate' ? 0.5 : 0;
67
69
  this.#signalBus.send('quality', 'ReverseGuard', severity, {
@@ -205,6 +207,34 @@ export class ReverseGuard {
205
207
  }
206
208
  return signals;
207
209
  }
210
+ /**
211
+ * 检查 recipe_source_refs 中是否有 stale 条目(与 SourceRefReconciler 数据交叉验证)
212
+ */
213
+ #checkSourceRefStaleness(recipeId) {
214
+ try {
215
+ const rows = this.#db
216
+ .prepare(`SELECT source_path FROM recipe_source_refs WHERE recipe_id = ? AND status = 'stale'`)
217
+ .all(recipeId);
218
+ if (rows.length === 0) {
219
+ return [];
220
+ }
221
+ return [
222
+ {
223
+ type: 'source_ref_stale',
224
+ detail: `${rows.length} source file(s) no longer exist: ${rows
225
+ .slice(0, 3)
226
+ .map((r) => r.source_path)
227
+ .join(', ')}${rows.length > 3 ? ` (+${rows.length - 3} more)` : ''}`,
228
+ severity: rows.length >= 3 ? 'high' : 'medium',
229
+ evidence: {},
230
+ },
231
+ ];
232
+ }
233
+ catch {
234
+ // recipe_source_refs 表可能不存在
235
+ return [];
236
+ }
237
+ }
208
238
  #extractSymbols(coreCode) {
209
239
  const symbols = new Set();
210
240
  for (const pattern of SYMBOL_PATTERNS) {
@@ -9,6 +9,7 @@
9
9
  * renamed — 文件已移动到 new_path,等待修复
10
10
  * stale — 路径失效,无法自动修复
11
11
  */
12
+ import type { SignalBus } from '../../infrastructure/signal/SignalBus.js';
12
13
  interface DatabaseLike {
13
14
  prepare(sql: string): {
14
15
  all(...params: unknown[]): Record<string, unknown>[];
@@ -46,6 +47,7 @@ export declare class SourceRefReconciler {
46
47
  #private;
47
48
  constructor(projectRoot: string, db: DatabaseLike, options?: {
48
49
  ttlMs?: number;
50
+ signalBus?: SignalBus;
49
51
  });
50
52
  /**
51
53
  * 从 knowledge_entries.reasoning 填充 recipe_source_refs 表。
@@ -21,11 +21,13 @@ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
21
21
  export class SourceRefReconciler {
22
22
  #projectRoot;
23
23
  #db;
24
+ #signalBus;
24
25
  #logger = Logger.getInstance();
25
26
  #ttlMs;
26
27
  constructor(projectRoot, db, options) {
27
28
  this.#projectRoot = projectRoot;
28
29
  this.#db = db;
30
+ this.#signalBus = options?.signalBus ?? null;
29
31
  this.#ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
30
32
  }
31
33
  /**
@@ -122,8 +124,44 @@ export class SourceRefReconciler {
122
124
  skipped: report.skipped,
123
125
  recipesProcessed: report.recipesProcessed,
124
126
  });
127
+ // 通过 SignalBus 发射信号 — 让 Governance 子系统感知 sourceRef 健康状况
128
+ if (this.#signalBus && report.stale > 0) {
129
+ this.#emitStaleSignals();
130
+ }
125
131
  return report;
126
132
  }
133
+ /**
134
+ * 为每个有 stale sourceRef 的 Recipe 发射 quality 信号。
135
+ * KnowledgeMetabolism 订阅 quality → 触发完整治理周期。
136
+ */
137
+ #emitStaleSignals() {
138
+ if (!this.#signalBus) {
139
+ return;
140
+ }
141
+ try {
142
+ const staleRecipes = this.#db
143
+ .prepare(`SELECT recipe_id, COUNT(*) AS stale_count,
144
+ (SELECT COUNT(*) FROM recipe_source_refs r2 WHERE r2.recipe_id = r.recipe_id) AS total_count
145
+ FROM recipe_source_refs r
146
+ WHERE status = 'stale'
147
+ GROUP BY recipe_id`)
148
+ .all();
149
+ for (const row of staleRecipes) {
150
+ const staleRatio = row.stale_count / row.total_count;
151
+ this.#signalBus.send('quality', 'SourceRefReconciler', staleRatio, {
152
+ target: row.recipe_id,
153
+ metadata: {
154
+ reason: 'source_ref_stale',
155
+ staleCount: row.stale_count,
156
+ totalRefs: row.total_count,
157
+ },
158
+ });
159
+ }
160
+ }
161
+ catch {
162
+ // 信号发射失败不影响主流程
163
+ }
164
+ }
127
165
  /**
128
166
  * 对 stale 条目尝试 git rename 修复。
129
167
  * 使用 execFile() 安全执行 git log(防止命令注入)。
@@ -160,6 +198,16 @@ export class SourceRefReconciler {
160
198
  renamed: report.renamed,
161
199
  stillStale: report.stillStale,
162
200
  });
201
+ // 修复成功 → 发射正向 quality 信号(value≈0 表示健康方向)
202
+ if (this.#signalBus) {
203
+ this.#signalBus.send('quality', 'SourceRefReconciler', 0.1, {
204
+ metadata: {
205
+ reason: 'source_ref_repaired',
206
+ renamed: report.renamed,
207
+ stillStale: report.stillStale,
208
+ },
209
+ });
210
+ }
163
211
  }
164
212
  return report;
165
213
  }
@@ -183,6 +183,7 @@ export declare const SubmitKnowledgeInput: z.ZodObject<{
183
183
  skipDuplicateCheck: z.ZodDefault<z.ZodBoolean>;
184
184
  client_id: z.ZodOptional<z.ZodString>;
185
185
  dimensionId: z.ZodOptional<z.ZodString>;
186
+ supersedes: z.ZodOptional<z.ZodString>;
186
187
  }, z.core.$strip>;
187
188
  export type SubmitKnowledgeInput = z.infer<typeof SubmitKnowledgeInput>;
188
189
  export declare const SkillInput: z.ZodObject<{
@@ -173,6 +173,10 @@ export const SubmitKnowledgeInput = z.object({
173
173
  skipDuplicateCheck: z.boolean().default(false),
174
174
  client_id: z.string().optional(),
175
175
  dimensionId: z.string().optional().describe('冷启动关联维度 ID'),
176
+ supersedes: z
177
+ .string()
178
+ .optional()
179
+ .describe('声明新 Recipe 替代旧 Recipe 的 ID。提交后系统将创建 supersede 提案,观察窗口内对比新旧表现后自动执行。'),
176
180
  });
177
181
  // ══════════════════════════════════════════════════════
178
182
  // 10. autosnippet_skill
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "3.3.4",
3
+ "version": "3.3.5",
4
4
  "description": "Extract code patterns into a knowledge base for AI coding assistants",
5
5
  "type": "module",
6
6
  "main": "dist/lib/bootstrap.js",