autosnippet 3.3.3 → 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.
- package/README.md +176 -81
- package/config/constitution.yaml +2 -0
- package/dist/lib/cli/KnowledgeSyncService.d.ts +5 -1
- package/dist/lib/cli/KnowledgeSyncService.js +5 -2
- package/dist/lib/domain/knowledge/values/Stats.d.ts +1 -1
- package/dist/lib/domain/knowledge/values/Stats.js +2 -2
- package/dist/lib/external/mcp/handlers/consolidated.js +178 -0
- package/dist/lib/external/mcp/handlers/task.js +36 -14
- package/dist/lib/external/mcp/tools.js +2 -1
- package/dist/lib/injection/modules/InfraModule.js +4 -1
- package/dist/lib/injection/modules/KnowledgeModule.js +23 -0
- package/dist/lib/repository/evolution/ProposalRepository.d.ts +99 -0
- package/dist/lib/repository/evolution/ProposalRepository.js +255 -0
- package/dist/lib/service/bootstrap/UiStartupTasks.d.ts +17 -4
- package/dist/lib/service/bootstrap/UiStartupTasks.js +53 -5
- package/dist/lib/service/evolution/DecayDetector.d.ts +4 -3
- package/dist/lib/service/evolution/DecayDetector.js +97 -22
- package/dist/lib/service/evolution/KnowledgeMetabolism.d.ts +4 -2
- package/dist/lib/service/evolution/KnowledgeMetabolism.js +29 -2
- package/dist/lib/service/evolution/ProposalExecutor.d.ts +62 -0
- package/dist/lib/service/evolution/ProposalExecutor.js +360 -0
- package/dist/lib/service/evolution/StagingManager.js +5 -3
- package/dist/lib/service/guard/GuardCrossFileChecks.js +2 -0
- package/dist/lib/service/guard/ReverseGuard.d.ts +1 -1
- package/dist/lib/service/guard/ReverseGuard.js +32 -2
- package/dist/lib/service/knowledge/SourceRefReconciler.d.ts +2 -0
- package/dist/lib/service/knowledge/SourceRefReconciler.js +48 -0
- package/dist/lib/service/task/IntentExtractor.d.ts +3 -1
- package/dist/lib/service/task/IntentExtractor.js +30 -10
- package/dist/lib/service/task/PrimeSearchPipeline.js +67 -12
- package/dist/lib/shared/schemas/mcp-tools.d.ts +2 -0
- package/dist/lib/shared/schemas/mcp-tools.js +9 -1
- package/package.json +1 -1
- package/templates/instructions/conventions.md +4 -2
|
@@ -20,6 +20,7 @@ export class KnowledgeMetabolism {
|
|
|
20
20
|
#decayDetector;
|
|
21
21
|
#signalBus;
|
|
22
22
|
#reportStore;
|
|
23
|
+
#proposalRepo;
|
|
23
24
|
#logger = Logger.getInstance();
|
|
24
25
|
#pendingTriggers = [];
|
|
25
26
|
#debounceTimer = null;
|
|
@@ -29,6 +30,7 @@ export class KnowledgeMetabolism {
|
|
|
29
30
|
this.#decayDetector = options.decayDetector;
|
|
30
31
|
this.#signalBus = options.signalBus ?? null;
|
|
31
32
|
this.#reportStore = options.reportStore ?? null;
|
|
33
|
+
this.#proposalRepo = options.proposalRepository ?? null;
|
|
32
34
|
// Phase 2: 订阅告警型信号,触发代谢周期
|
|
33
35
|
if (this.#signalBus) {
|
|
34
36
|
this.#signalBus.subscribe('decay|quality|anomaly', (signal) => {
|
|
@@ -66,7 +68,31 @@ export class KnowledgeMetabolism {
|
|
|
66
68
|
...this.#proposalsFromRedundancies(redundancies),
|
|
67
69
|
...this.#proposalsFromDecay(decayResults),
|
|
68
70
|
];
|
|
69
|
-
// 5.
|
|
71
|
+
// 5. 持久化提案到 evolution_proposals 表
|
|
72
|
+
let persistedCount = 0;
|
|
73
|
+
if (this.#proposalRepo && proposals.length > 0) {
|
|
74
|
+
for (const p of proposals) {
|
|
75
|
+
const sourceMap = {
|
|
76
|
+
contradiction: 'metabolism',
|
|
77
|
+
redundancy: 'metabolism',
|
|
78
|
+
decay: 'decay-scan',
|
|
79
|
+
enhancement: 'metabolism',
|
|
80
|
+
};
|
|
81
|
+
const record = this.#proposalRepo.create({
|
|
82
|
+
type: p.type,
|
|
83
|
+
targetRecipeId: p.targetRecipeId,
|
|
84
|
+
relatedRecipeIds: p.relatedRecipeIds,
|
|
85
|
+
confidence: p.confidence,
|
|
86
|
+
source: sourceMap[p.source] ?? 'metabolism',
|
|
87
|
+
description: p.description,
|
|
88
|
+
evidence: p.evidence.map((e) => ({ detail: e })),
|
|
89
|
+
});
|
|
90
|
+
if (record) {
|
|
91
|
+
persistedCount++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 6. 写入治理报告(降级:同时写 ReportStore)
|
|
70
96
|
if (this.#reportStore && proposals.length > 0) {
|
|
71
97
|
void this.#reportStore.write({
|
|
72
98
|
category: 'governance',
|
|
@@ -74,6 +100,7 @@ export class KnowledgeMetabolism {
|
|
|
74
100
|
producer: 'KnowledgeMetabolism',
|
|
75
101
|
data: {
|
|
76
102
|
proposalCount: proposals.length,
|
|
103
|
+
persistedCount,
|
|
77
104
|
contradictionCount: contradictions.length,
|
|
78
105
|
redundancyCount: redundancies.length,
|
|
79
106
|
decayingCount: decayResults.filter((d) => d.level !== 'healthy' && d.level !== 'watch')
|
|
@@ -121,7 +148,7 @@ export class KnowledgeMetabolism {
|
|
|
121
148
|
#proposalsFromContradictions(results) {
|
|
122
149
|
const now = Date.now();
|
|
123
150
|
return results.map((r) => ({
|
|
124
|
-
type: r.type === 'hard' ? '
|
|
151
|
+
type: r.type === 'hard' ? 'contradiction' : 'correction',
|
|
125
152
|
targetRecipeId: r.recipeA,
|
|
126
153
|
relatedRecipeIds: [r.recipeB],
|
|
127
154
|
confidence: r.confidence,
|
|
@@ -0,0 +1,62 @@
|
|
|
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 type { SignalBus } from '../../infrastructure/signal/SignalBus.js';
|
|
19
|
+
import type { ProposalRepository, ProposalType } from '../../repository/evolution/ProposalRepository.js';
|
|
20
|
+
interface DatabaseLike {
|
|
21
|
+
prepare(sql: string): {
|
|
22
|
+
all(...params: unknown[]): Record<string, unknown>[];
|
|
23
|
+
get(...params: unknown[]): Record<string, unknown> | undefined;
|
|
24
|
+
run(...params: unknown[]): {
|
|
25
|
+
changes: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface ProposalExecutionResult {
|
|
30
|
+
executed: {
|
|
31
|
+
id: string;
|
|
32
|
+
type: ProposalType;
|
|
33
|
+
targetRecipeId: string;
|
|
34
|
+
}[];
|
|
35
|
+
rejected: {
|
|
36
|
+
id: string;
|
|
37
|
+
type: ProposalType;
|
|
38
|
+
reason: string;
|
|
39
|
+
}[];
|
|
40
|
+
expired: {
|
|
41
|
+
id: string;
|
|
42
|
+
type: ProposalType;
|
|
43
|
+
}[];
|
|
44
|
+
skipped: {
|
|
45
|
+
id: string;
|
|
46
|
+
type: ProposalType;
|
|
47
|
+
reason: string;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
export declare class ProposalExecutor {
|
|
51
|
+
#private;
|
|
52
|
+
constructor(db: DatabaseLike, repo: ProposalRepository, options?: {
|
|
53
|
+
signalBus?: SignalBus;
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* 定期调用(UiStartupTasks Stage 5)
|
|
57
|
+
*
|
|
58
|
+
* 扫描所有到期 Proposal → 评估 → 执行/拒绝/过期
|
|
59
|
+
*/
|
|
60
|
+
checkAndExecute(): ProposalExecutionResult;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
@@ -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),
|
|
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),
|
|
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(
|
|
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
|
-
//
|
|
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
|
}
|