@dommaker/harness 0.13.3 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/bin/harness-knowledge-check.js +38 -23
- package/bin/harness-knowledge-track.js +30 -4
- package/bin/harness.js +24 -1
- package/dist/cli/commands/diagnose.d.ts +16 -0
- package/dist/cli/commands/diagnose.d.ts.map +1 -0
- package/dist/cli/commands/diagnose.js +195 -0
- package/dist/cli/commands/diagnose.js.map +1 -0
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +4 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/knowledge.d.ts +21 -0
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +155 -4
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/propose.d.ts +18 -0
- package/dist/cli/commands/propose.d.ts.map +1 -0
- package/dist/cli/commands/propose.js +331 -0
- package/dist/cli/commands/propose.js.map +1 -0
- package/dist/cli/commands/traces.d.ts +15 -0
- package/dist/cli/commands/traces.d.ts.map +1 -0
- package/dist/cli/commands/traces.js +167 -0
- package/dist/cli/commands/traces.js.map +1 -0
- package/dist/constraints/definitions.d.ts +39 -0
- package/dist/constraints/definitions.d.ts.map +1 -0
- package/dist/constraints/definitions.js +71 -0
- package/dist/constraints/definitions.js.map +1 -0
- package/dist/constraints/quality.d.ts +11 -0
- package/dist/constraints/quality.d.ts.map +1 -0
- package/dist/constraints/quality.js +257 -0
- package/dist/constraints/quality.js.map +1 -0
- package/dist/constraints/safety.d.ts +13 -0
- package/dist/constraints/safety.d.ts.map +1 -0
- package/dist/constraints/safety.js +103 -0
- package/dist/constraints/safety.js.map +1 -0
- package/dist/core/constraints/definitions.d.ts.map +1 -1
- package/dist/core/constraints/definitions.js +4 -2
- package/dist/core/constraints/definitions.js.map +1 -1
- package/dist/extensions/long-running/constraints.d.ts +38 -0
- package/dist/extensions/long-running/constraints.d.ts.map +1 -0
- package/dist/extensions/long-running/constraints.js +153 -0
- package/dist/extensions/long-running/constraints.js.map +1 -0
- package/dist/extensions/long-running/index.d.ts +30 -0
- package/dist/extensions/long-running/index.d.ts.map +1 -0
- package/dist/extensions/long-running/index.js +50 -0
- package/dist/extensions/long-running/index.js.map +1 -0
- package/dist/extensions/long-running/types.d.ts +154 -0
- package/dist/extensions/long-running/types.d.ts.map +1 -0
- package/dist/extensions/long-running/types.js +9 -0
- package/dist/extensions/long-running/types.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/knowledge/audit.d.ts +76 -0
- package/dist/knowledge/audit.d.ts.map +1 -0
- package/dist/knowledge/audit.js +566 -0
- package/dist/knowledge/audit.js.map +1 -0
- package/dist/knowledge/import.d.ts.map +1 -1
- package/dist/knowledge/import.js +3 -0
- package/dist/knowledge/import.js.map +1 -1
- package/dist/knowledge/index.d.ts +5 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +6 -1
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/ingest.d.ts +27 -0
- package/dist/knowledge/ingest.d.ts.map +1 -1
- package/dist/knowledge/ingest.js +150 -5
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
- package/dist/knowledge/lifecycle-hooks.js +3 -0
- package/dist/knowledge/lifecycle-hooks.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +59 -7
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +213 -52
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/knowledge/lint.d.ts.map +1 -1
- package/dist/knowledge/lint.js +43 -2
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/migration.d.ts +25 -0
- package/dist/knowledge/migration.d.ts.map +1 -0
- package/dist/knowledge/migration.js +96 -0
- package/dist/knowledge/migration.js.map +1 -0
- package/dist/knowledge/query.d.ts +29 -1
- package/dist/knowledge/query.d.ts.map +1 -1
- package/dist/knowledge/query.js +58 -0
- package/dist/knowledge/query.js.map +1 -1
- package/dist/knowledge/store.d.ts +21 -1
- package/dist/knowledge/store.d.ts.map +1 -1
- package/dist/knowledge/store.js +103 -6
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +29 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/types.js.map +1 -1
- package/dist/presets/long-running.d.ts +33 -0
- package/dist/presets/long-running.d.ts.map +1 -0
- package/dist/presets/long-running.js +52 -0
- package/dist/presets/long-running.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/__tests__/knowledge.test.ts +167 -0
- package/src/cli/commands/index.ts +1 -1
- package/src/cli/commands/knowledge.ts +175 -4
- package/src/context/__tests__/knowledge-injector.test.ts +3 -0
- package/src/core/constraints/definitions.ts +4 -2
- package/src/dashboard/__tests__/data.test.ts +3 -0
- package/src/dashboard/__tests__/stats.test.ts +3 -0
- package/src/index.ts +3 -1
- package/src/knowledge/CONTEXT.md +29 -7
- package/src/knowledge/__tests__/audit.test.ts +598 -0
- package/src/knowledge/__tests__/doctor.test.ts +27 -0
- package/src/knowledge/__tests__/ingest.test.ts +319 -0
- package/src/knowledge/__tests__/lifecycle-hooks.test.ts +3 -0
- package/src/knowledge/__tests__/lifecycle.test.ts +655 -2
- package/src/knowledge/__tests__/lint.test.ts +51 -0
- package/src/knowledge/__tests__/migration.test.ts +115 -0
- package/src/knowledge/__tests__/query.test.ts +86 -0
- package/src/knowledge/__tests__/reference-tracker.test.ts +3 -0
- package/src/knowledge/__tests__/store.test.ts +207 -0
- package/src/knowledge/audit.ts +640 -0
- package/src/knowledge/import.ts +3 -0
- package/src/knowledge/index.ts +5 -1
- package/src/knowledge/ingest.ts +169 -5
- package/src/knowledge/lifecycle-hooks.ts +3 -0
- package/src/knowledge/lifecycle.ts +218 -49
- package/src/knowledge/lint.ts +43 -2
- package/src/knowledge/migration.ts +73 -0
- package/src/knowledge/query.ts +71 -0
- package/src/knowledge/store.ts +100 -6
- package/src/knowledge/types.ts +33 -1
- package/src/monitoring/__tests__/knowledge-doctor.test.ts +3 -0
- package/src/monitoring/__tests__/knowledge-evolver.test.ts +3 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识库质量审计引擎
|
|
3
|
+
*
|
|
4
|
+
* 纯代码检测,零 token 成本。
|
|
5
|
+
* 6 维度覆盖:结构完整性 / 内容质量 / 去重有效性 / 成熟度健康 / 新鲜度 / 飞轮验证
|
|
6
|
+
*
|
|
7
|
+
* 两种模式:
|
|
8
|
+
* - validate(entry): 单条入库检查(ingest gate)
|
|
9
|
+
* - run(autoFix): 全量扫描(日兜底 / 手动)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { KnowledgeStore } from './store';
|
|
15
|
+
import type { KnowledgeEntry, MaturityLevel } from './types';
|
|
16
|
+
|
|
17
|
+
// ── Types ─────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export type AuditRuleName =
|
|
20
|
+
// D1: 结构完整性
|
|
21
|
+
| 'frontmatter-missing'
|
|
22
|
+
// D2: 内容质量
|
|
23
|
+
| 'test-data-pollution'
|
|
24
|
+
| 'daily-audit-noise'
|
|
25
|
+
| 'event-noise'
|
|
26
|
+
| 'zero-content-proven'
|
|
27
|
+
| 'short-content'
|
|
28
|
+
| 'maturity-inflation'
|
|
29
|
+
// D3: 去重有效性
|
|
30
|
+
| 'title-duplicate'
|
|
31
|
+
| 'source-refs-bloat'
|
|
32
|
+
// D4: 成熟度健康
|
|
33
|
+
| 'promotion-blocked'
|
|
34
|
+
| 'orphan-draft'
|
|
35
|
+
// D5: 新鲜度
|
|
36
|
+
| 'stale-entry';
|
|
37
|
+
|
|
38
|
+
export type AuditAction = 'archive' | 'demote' | 'flag' | 'reject' | 'trim';
|
|
39
|
+
|
|
40
|
+
export interface AuditIssue {
|
|
41
|
+
rule: AuditRuleName;
|
|
42
|
+
entryId: string;
|
|
43
|
+
title: string;
|
|
44
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
45
|
+
action: AuditAction;
|
|
46
|
+
detail: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DimensionMetrics {
|
|
50
|
+
score: number; // 0-100
|
|
51
|
+
issues: number;
|
|
52
|
+
details: Record<string, number | string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AuditReport {
|
|
56
|
+
timestamp: string;
|
|
57
|
+
totalEntries: number;
|
|
58
|
+
issues: AuditIssue[];
|
|
59
|
+
summary: Record<AuditRuleName, number>;
|
|
60
|
+
dimensions: {
|
|
61
|
+
structure: DimensionMetrics;
|
|
62
|
+
content: DimensionMetrics;
|
|
63
|
+
dedup: DimensionMetrics;
|
|
64
|
+
maturity: DimensionMetrics;
|
|
65
|
+
freshness: DimensionMetrics;
|
|
66
|
+
flywheel: DimensionMetrics;
|
|
67
|
+
incremental: DimensionMetrics;
|
|
68
|
+
};
|
|
69
|
+
autoFixed: number;
|
|
70
|
+
healthScore: { before: number; after: number };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AuditOptions {
|
|
74
|
+
baseDir?: string;
|
|
75
|
+
autoFix?: boolean;
|
|
76
|
+
shortContentThreshold?: number;
|
|
77
|
+
staleDays?: number;
|
|
78
|
+
promotionBlockDays?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Constants ─────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const DEFAULT_SHORT_CONTENT_THRESHOLD = 50;
|
|
84
|
+
const ZERO_CONTENT_THRESHOLD = 20;
|
|
85
|
+
const DEFAULT_STALE_DAYS = 90;
|
|
86
|
+
const DEFAULT_PROMOTION_BLOCK_DAYS = 30;
|
|
87
|
+
const MAX_SOURCE_REFS = 20;
|
|
88
|
+
|
|
89
|
+
const TEST_TAG_PATTERNS = [/^test-scope-/, /^test-empty-/, /^test-\d/];
|
|
90
|
+
const DAILY_AUDIT_PATTERN = /^\[Auditor\] Daily audit/;
|
|
91
|
+
const EVENT_NOISE_PATTERNS = [
|
|
92
|
+
/^\[Monitor\]\s/,
|
|
93
|
+
/^KnowledgeSync cycle:/,
|
|
94
|
+
/^\[Triage Fix\]\s/,
|
|
95
|
+
/^\[Session Feature\]\s/,
|
|
96
|
+
];
|
|
97
|
+
const REQUIRED_FRONTMATTER = ['id', 'type', 'title', 'maturity'];
|
|
98
|
+
|
|
99
|
+
// ── Per-Entry Rules ───────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
interface AuditRule {
|
|
102
|
+
name: AuditRuleName;
|
|
103
|
+
severity: AuditIssue['severity'];
|
|
104
|
+
action: AuditAction;
|
|
105
|
+
detect: (entry: KnowledgeEntry, ctx: AuditContext) => string | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface AuditContext {
|
|
109
|
+
shortContentThreshold: number;
|
|
110
|
+
staleDays: number;
|
|
111
|
+
promotionBlockDays: number;
|
|
112
|
+
allEntries?: KnowledgeEntry[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const perEntryRules: AuditRule[] = [
|
|
116
|
+
// D1: 结构完整性
|
|
117
|
+
{
|
|
118
|
+
name: 'frontmatter-missing',
|
|
119
|
+
severity: 'high',
|
|
120
|
+
action: 'reject',
|
|
121
|
+
detect: (entry) => {
|
|
122
|
+
const missing = REQUIRED_FRONTMATTER.filter(f => {
|
|
123
|
+
const val = (entry as any)[f];
|
|
124
|
+
return val === undefined || val === null || val === '';
|
|
125
|
+
});
|
|
126
|
+
if (missing.length > 0) {
|
|
127
|
+
return `缺少必填字段: ${missing.join(', ')}`;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// D2: 内容质量
|
|
134
|
+
{
|
|
135
|
+
name: 'test-data-pollution',
|
|
136
|
+
severity: 'critical',
|
|
137
|
+
action: 'archive',
|
|
138
|
+
detect: (entry) => {
|
|
139
|
+
if (entry.maturity === 'archived') return null;
|
|
140
|
+
// Match test ID patterns
|
|
141
|
+
if (/^(test-|inj-test)/.test(entry.id)) {
|
|
142
|
+
return `测试 ID: "${entry.id}"`;
|
|
143
|
+
}
|
|
144
|
+
const hasTestTag = (entry.tags ?? []).some(t => TEST_TAG_PATTERNS.some(p => p.test(t)));
|
|
145
|
+
if (hasTestTag) {
|
|
146
|
+
return `测试标签: ${(entry.tags ?? []).filter(t => TEST_TAG_PATTERNS.some(p => p.test(t))).join(', ')}`;
|
|
147
|
+
}
|
|
148
|
+
if (/^(Test Entry|Test pattern|Test incident|Empty Test)$/i.test(entry.title.trim())) {
|
|
149
|
+
return `测试标题: "${entry.title}"`;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'daily-audit-noise',
|
|
156
|
+
severity: 'high',
|
|
157
|
+
action: 'archive',
|
|
158
|
+
detect: (entry) => {
|
|
159
|
+
if (entry.maturity === 'archived') return null;
|
|
160
|
+
if (DAILY_AUDIT_PATTERN.test(entry.title)) {
|
|
161
|
+
return `每日审计摘要: "${entry.title}"`;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'event-noise',
|
|
168
|
+
severity: 'critical',
|
|
169
|
+
action: 'archive',
|
|
170
|
+
detect: (entry) => {
|
|
171
|
+
if (entry.maturity === 'archived') return null;
|
|
172
|
+
for (const pattern of EVENT_NOISE_PATTERNS) {
|
|
173
|
+
if (pattern.test(entry.title)) {
|
|
174
|
+
return `运维事件标题: "${entry.title}"`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'zero-content-proven',
|
|
182
|
+
severity: 'critical',
|
|
183
|
+
action: 'demote',
|
|
184
|
+
detect: (entry) => {
|
|
185
|
+
if (entry.maturity === 'proven' && entry.content.trim().length < ZERO_CONTENT_THRESHOLD) {
|
|
186
|
+
return `proven 条目内容仅 ${entry.content.trim().length} 字符`;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'maturity-inflation',
|
|
193
|
+
severity: 'high',
|
|
194
|
+
action: 'demote',
|
|
195
|
+
detect: (entry) => {
|
|
196
|
+
if (entry.maturity === 'verified' && entry.content.trim().length < ZERO_CONTENT_THRESHOLD) {
|
|
197
|
+
return `verified 条目内容仅 ${entry.content.trim().length} 字符`;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'short-content',
|
|
204
|
+
severity: 'medium',
|
|
205
|
+
action: 'flag',
|
|
206
|
+
detect: (entry, ctx) => {
|
|
207
|
+
if (entry.maturity === 'archived') return null;
|
|
208
|
+
const len = entry.content.trim().length;
|
|
209
|
+
if (len < ctx.shortContentThreshold && len >= ZERO_CONTENT_THRESHOLD) {
|
|
210
|
+
return `内容 ${len} 字符 (阈值 ${ctx.shortContentThreshold})`;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// D3: 去重有效性
|
|
217
|
+
{
|
|
218
|
+
name: 'title-duplicate',
|
|
219
|
+
severity: 'medium',
|
|
220
|
+
action: 'flag',
|
|
221
|
+
detect: (entry, ctx) => {
|
|
222
|
+
if (entry.maturity === 'archived') return null;
|
|
223
|
+
if (!ctx.allEntries) return null;
|
|
224
|
+
const dupes = ctx.allEntries.filter(e =>
|
|
225
|
+
e.id !== entry.id &&
|
|
226
|
+
e.maturity !== 'archived' &&
|
|
227
|
+
e.type === entry.type &&
|
|
228
|
+
e.title.toLowerCase().trim() === entry.title.toLowerCase().trim()
|
|
229
|
+
);
|
|
230
|
+
if (dupes.length > 0) {
|
|
231
|
+
return `与 ${dupes.map(e => e.id).join(', ')} 标题重复`;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'source-refs-bloat',
|
|
238
|
+
severity: 'low',
|
|
239
|
+
action: 'trim',
|
|
240
|
+
detect: (entry) => {
|
|
241
|
+
if ((entry.sourceReferences?.length || 0) > MAX_SOURCE_REFS) {
|
|
242
|
+
return `sourceReferences ${entry.sourceReferences!.length} 条 (上限 ${MAX_SOURCE_REFS})`;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// D4: 成熟度健康
|
|
249
|
+
{
|
|
250
|
+
name: 'promotion-blocked',
|
|
251
|
+
severity: 'medium',
|
|
252
|
+
action: 'flag',
|
|
253
|
+
detect: (entry, ctx) => {
|
|
254
|
+
if (entry.maturity !== 'draft') return null;
|
|
255
|
+
const created = new Date(entry.created);
|
|
256
|
+
const daysSinceCreated = (Date.now() - created.getTime()) / (1000 * 60 * 60 * 24);
|
|
257
|
+
if (daysSinceCreated > ctx.promotionBlockDays && !entry.lastReferenced) {
|
|
258
|
+
return `draft 已 ${Math.floor(daysSinceCreated)} 天未被引用`;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'orphan-draft',
|
|
265
|
+
severity: 'low',
|
|
266
|
+
action: 'flag',
|
|
267
|
+
detect: (entry) => {
|
|
268
|
+
if (entry.maturity !== 'draft') return null;
|
|
269
|
+
if (entry.contributors.length === 0 && entry.projects.length === 0 && entry.referencedBy.length === 0) {
|
|
270
|
+
return `draft 无贡献者/项目/引用`;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// D5: 新鲜度
|
|
277
|
+
{
|
|
278
|
+
name: 'stale-entry',
|
|
279
|
+
severity: 'medium',
|
|
280
|
+
action: 'flag',
|
|
281
|
+
detect: (entry, ctx) => {
|
|
282
|
+
if (entry.maturity === 'archived') return null;
|
|
283
|
+
const lastRef = entry.lastReferenced || entry.created;
|
|
284
|
+
if (!lastRef) return null;
|
|
285
|
+
const daysSinceRef = (Date.now() - new Date(lastRef).getTime()) / (1000 * 60 * 60 * 24);
|
|
286
|
+
if (daysSinceRef > ctx.staleDays) {
|
|
287
|
+
return `超过 ${Math.floor(daysSinceRef)} 天未引用 (阈值 ${ctx.staleDays})`;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
// ── Audit Engine ──────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
export class KnowledgeAudit {
|
|
297
|
+
private store: KnowledgeStore;
|
|
298
|
+
private shortContentThreshold: number;
|
|
299
|
+
private staleDays: number;
|
|
300
|
+
private promotionBlockDays: number;
|
|
301
|
+
|
|
302
|
+
constructor(options?: AuditOptions) {
|
|
303
|
+
this.store = new KnowledgeStore({ baseDir: options?.baseDir });
|
|
304
|
+
this.shortContentThreshold = options?.shortContentThreshold ?? DEFAULT_SHORT_CONTENT_THRESHOLD;
|
|
305
|
+
this.staleDays = options?.staleDays ?? DEFAULT_STALE_DAYS;
|
|
306
|
+
this.promotionBlockDays = options?.promotionBlockDays ?? DEFAULT_PROMOTION_BLOCK_DAYS;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 单条入库检查(ingest gate 模式)
|
|
311
|
+
*/
|
|
312
|
+
validate(entry: KnowledgeEntry): AuditIssue[] {
|
|
313
|
+
const ctx: AuditContext = {
|
|
314
|
+
shortContentThreshold: this.shortContentThreshold,
|
|
315
|
+
staleDays: this.staleDays,
|
|
316
|
+
promotionBlockDays: this.promotionBlockDays,
|
|
317
|
+
};
|
|
318
|
+
const issues: AuditIssue[] = [];
|
|
319
|
+
for (const rule of perEntryRules) {
|
|
320
|
+
const detail = rule.detect(entry, ctx);
|
|
321
|
+
if (detail) {
|
|
322
|
+
issues.push({
|
|
323
|
+
rule: rule.name,
|
|
324
|
+
entryId: entry.id,
|
|
325
|
+
title: entry.title,
|
|
326
|
+
severity: rule.severity,
|
|
327
|
+
action: rule.action,
|
|
328
|
+
detail,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return issues;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 全量扫描(日兜底 / 手动模式)
|
|
337
|
+
*/
|
|
338
|
+
run(options?: { autoFix?: boolean }): AuditReport {
|
|
339
|
+
const entries = this.store.list({ excludeArchived: false });
|
|
340
|
+
const ctx: AuditContext = {
|
|
341
|
+
shortContentThreshold: this.shortContentThreshold,
|
|
342
|
+
staleDays: this.staleDays,
|
|
343
|
+
promotionBlockDays: this.promotionBlockDays,
|
|
344
|
+
allEntries: entries,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Per-entry issues
|
|
348
|
+
const allIssues: AuditIssue[] = [];
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
for (const rule of perEntryRules) {
|
|
351
|
+
const detail = rule.detect(entry, ctx);
|
|
352
|
+
if (detail) {
|
|
353
|
+
allIssues.push({
|
|
354
|
+
rule: rule.name,
|
|
355
|
+
entryId: entry.id,
|
|
356
|
+
title: entry.title,
|
|
357
|
+
severity: rule.severity,
|
|
358
|
+
action: rule.action,
|
|
359
|
+
detail,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Summary
|
|
366
|
+
const summary = {} as Record<AuditRuleName, number>;
|
|
367
|
+
for (const rule of perEntryRules) summary[rule.name] = 0;
|
|
368
|
+
for (const issue of allIssues) summary[issue.rule]++;
|
|
369
|
+
|
|
370
|
+
// Dimension metrics
|
|
371
|
+
const dimensions = this.computeDimensions(entries, allIssues);
|
|
372
|
+
|
|
373
|
+
// Health score (before)
|
|
374
|
+
const healthBefore = this.calculateHealthScore(entries, allIssues);
|
|
375
|
+
|
|
376
|
+
// Auto-fix
|
|
377
|
+
let autoFixed = 0;
|
|
378
|
+
if (options?.autoFix) {
|
|
379
|
+
autoFixed = this.applyFixes(allIssues);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Health score (after)
|
|
383
|
+
const entriesAfter = options?.autoFix ? this.store.list({ excludeArchived: false }) : entries;
|
|
384
|
+
const issuesAfter = options?.autoFix ? this.runScan(entriesAfter, ctx) : allIssues;
|
|
385
|
+
const healthAfter = this.calculateHealthScore(entriesAfter, issuesAfter);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
timestamp: new Date().toISOString(),
|
|
389
|
+
totalEntries: entries.length,
|
|
390
|
+
issues: allIssues,
|
|
391
|
+
summary,
|
|
392
|
+
dimensions,
|
|
393
|
+
autoFixed,
|
|
394
|
+
healthScore: { before: healthBefore, after: healthAfter },
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ── Dimensions ────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
private computeDimensions(entries: KnowledgeEntry[], issues: AuditIssue[]): AuditReport['dimensions'] {
|
|
401
|
+
const active = entries.filter(e => e.maturity !== 'archived');
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
|
|
404
|
+
// D1: 结构完整性
|
|
405
|
+
const d1Issues = issues.filter(i => i.rule === 'frontmatter-missing');
|
|
406
|
+
const d1Score = entries.length === 0 ? 100 : Math.max(0, 100 - (d1Issues.length / entries.length) * 100);
|
|
407
|
+
|
|
408
|
+
// D2: 内容质量
|
|
409
|
+
const d2Rules: AuditRuleName[] = ['test-data-pollution', 'daily-audit-noise', 'event-noise', 'zero-content-proven', 'short-content', 'maturity-inflation'];
|
|
410
|
+
const d2Issues = issues.filter(i => d2Rules.includes(i.rule));
|
|
411
|
+
const d2Score = active.length === 0 ? 100 : Math.max(0, 100 - (d2Issues.length / active.length) * 100);
|
|
412
|
+
|
|
413
|
+
// D3: 去重有效性
|
|
414
|
+
const d3Rules: AuditRuleName[] = ['title-duplicate', 'source-refs-bloat'];
|
|
415
|
+
const d3Issues = issues.filter(i => d3Rules.includes(i.rule));
|
|
416
|
+
const d3Score = active.length === 0 ? 100 : Math.max(0, 100 - (d3Issues.length / active.length) * 100);
|
|
417
|
+
|
|
418
|
+
// D4: 成熟度健康
|
|
419
|
+
const d4Rules: AuditRuleName[] = ['promotion-blocked', 'orphan-draft'];
|
|
420
|
+
const d4Issues = issues.filter(i => d4Rules.includes(i.rule));
|
|
421
|
+
const byMaturity: Record<string, number> = {};
|
|
422
|
+
for (const e of entries) byMaturity[e.maturity] = (byMaturity[e.maturity] || 0) + 1;
|
|
423
|
+
const draftRatio = entries.length > 0 ? (byMaturity['draft'] || 0) / entries.length : 0;
|
|
424
|
+
const provenRatio = entries.length > 0 ? (byMaturity['proven'] || 0) / entries.length : 0;
|
|
425
|
+
// Penalize: draft >50% or proven <2% or proven >20% (test inflation)
|
|
426
|
+
let d4Penalty = d4Issues.length * 2;
|
|
427
|
+
if (draftRatio > 0.5) d4Penalty += 10;
|
|
428
|
+
if (provenRatio < 0.02 && entries.length > 20) d4Penalty += 5;
|
|
429
|
+
if (provenRatio > 0.2) d4Penalty += 5; // likely test inflation
|
|
430
|
+
const d4Score = entries.length === 0 ? 100 : Math.max(0, 100 - (d4Penalty / entries.length) * 100);
|
|
431
|
+
|
|
432
|
+
// D5: 新鲜度
|
|
433
|
+
const d5Issues = issues.filter(i => i.rule === 'stale-entry');
|
|
434
|
+
const ages = active.map(e => {
|
|
435
|
+
const lastRef = e.lastReferenced || e.created;
|
|
436
|
+
return lastRef ? (now - new Date(lastRef).getTime()) / (1000 * 60 * 60 * 24) : 999;
|
|
437
|
+
});
|
|
438
|
+
const avgAge = ages.length > 0 ? ages.reduce((a, b) => a + b, 0) / ages.length : 0;
|
|
439
|
+
const staleRatio = active.length > 0 ? d5Issues.length / active.length : 0;
|
|
440
|
+
const d5Score = active.length === 0 ? 100 : Math.max(0, 100 - (staleRatio * 100));
|
|
441
|
+
|
|
442
|
+
// D6: 飞轮验证
|
|
443
|
+
const withRefs = active.filter(e => e.referencedBy.length > 0).length;
|
|
444
|
+
const refCoverage = active.length > 0 ? withRefs / active.length : 0;
|
|
445
|
+
const avgRefs = active.length > 0
|
|
446
|
+
? active.reduce((sum, e) => sum + e.referencedBy.length, 0) / active.length
|
|
447
|
+
: 0;
|
|
448
|
+
|
|
449
|
+
// Consumption hit rate from aggregated stats file (written by MonitorAgent)
|
|
450
|
+
let consumptionHitRate = 0;
|
|
451
|
+
let dailyConsumptionEvents = 0;
|
|
452
|
+
try {
|
|
453
|
+
const statsPath = path.join(this.store.getBaseDir(), '.consumption-stats.json');
|
|
454
|
+
if (fs.existsSync(statsPath)) {
|
|
455
|
+
const stats = JSON.parse(fs.readFileSync(statsPath, 'utf-8'));
|
|
456
|
+
dailyConsumptionEvents = stats.dailyEvents || 0;
|
|
457
|
+
// hitRate: daily events / active entries, capped at 1
|
|
458
|
+
consumptionHitRate = active.length > 0
|
|
459
|
+
? Math.min(dailyConsumptionEvents / active.length, 1)
|
|
460
|
+
: 0;
|
|
461
|
+
}
|
|
462
|
+
} catch { /* best-effort */ }
|
|
463
|
+
|
|
464
|
+
// Score: refCoverage * 50 + avgRefs * 20 + consumptionHitRate * 30
|
|
465
|
+
const d6Score = Math.min(100, Math.round(
|
|
466
|
+
refCoverage * 50 +
|
|
467
|
+
Math.min(avgRefs / 5, 1) * 20 +
|
|
468
|
+
consumptionHitRate * 30
|
|
469
|
+
));
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
structure: {
|
|
473
|
+
score: Math.round(d1Score),
|
|
474
|
+
issues: d1Issues.length,
|
|
475
|
+
details: { invalidEntries: d1Issues.length },
|
|
476
|
+
},
|
|
477
|
+
content: {
|
|
478
|
+
score: Math.round(d2Score),
|
|
479
|
+
issues: d2Issues.length,
|
|
480
|
+
details: {
|
|
481
|
+
testData: issues.filter(i => i.rule === 'test-data-pollution').length,
|
|
482
|
+
dailyAudit: issues.filter(i => i.rule === 'daily-audit-noise').length,
|
|
483
|
+
zeroContent: issues.filter(i => i.rule === 'zero-content-proven').length,
|
|
484
|
+
shortContent: issues.filter(i => i.rule === 'short-content').length,
|
|
485
|
+
maturityInflation: issues.filter(i => i.rule === 'maturity-inflation').length,
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
dedup: {
|
|
489
|
+
score: Math.round(d3Score),
|
|
490
|
+
issues: d3Issues.length,
|
|
491
|
+
details: {
|
|
492
|
+
titleDuplicates: issues.filter(i => i.rule === 'title-duplicate').length,
|
|
493
|
+
sourceRefsBloat: issues.filter(i => i.rule === 'source-refs-bloat').length,
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
maturity: {
|
|
497
|
+
score: Math.round(d4Score),
|
|
498
|
+
issues: d4Issues.length,
|
|
499
|
+
details: {
|
|
500
|
+
draft: byMaturity['draft'] || 0,
|
|
501
|
+
verified: byMaturity['verified'] || 0,
|
|
502
|
+
proven: byMaturity['proven'] || 0,
|
|
503
|
+
archived: byMaturity['archived'] || 0,
|
|
504
|
+
draftRatio: Math.round(draftRatio * 100),
|
|
505
|
+
provenRatio: Math.round(provenRatio * 100),
|
|
506
|
+
promotionBlocked: issues.filter(i => i.rule === 'promotion-blocked').length,
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
freshness: {
|
|
510
|
+
score: Math.round(d5Score),
|
|
511
|
+
issues: d5Issues.length,
|
|
512
|
+
details: {
|
|
513
|
+
avgAgeDays: Math.round(avgAge),
|
|
514
|
+
staleEntries: d5Issues.length,
|
|
515
|
+
staleRatio: Math.round(staleRatio * 100),
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
flywheel: {
|
|
519
|
+
score: d6Score,
|
|
520
|
+
issues: 0,
|
|
521
|
+
details: {
|
|
522
|
+
activeEntries: active.length,
|
|
523
|
+
entriesWithRefs: withRefs,
|
|
524
|
+
refCoverage: Math.round(refCoverage * 100),
|
|
525
|
+
avgRefCount: Math.round(avgRefs * 10) / 10,
|
|
526
|
+
dailyConsumptionEvents,
|
|
527
|
+
consumptionHitRate: Math.round(consumptionHitRate * 100),
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
incremental: this.computeIncremental(),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private computeIncremental(): DimensionMetrics {
|
|
535
|
+
const survival = this.store.getSurvivalRate(30);
|
|
536
|
+
if (!survival) {
|
|
537
|
+
return { score: 100, issues: 0, details: { note: 'no 30d snapshot available' } };
|
|
538
|
+
}
|
|
539
|
+
// Score = survival rate directly (80% target → 80/100)
|
|
540
|
+
return {
|
|
541
|
+
score: survival.rate,
|
|
542
|
+
issues: survival.total - survival.survived,
|
|
543
|
+
details: {
|
|
544
|
+
survivalRate: survival.rate,
|
|
545
|
+
survived: survival.survived,
|
|
546
|
+
total: survival.total,
|
|
547
|
+
snapshotDate: survival.snapshotDate,
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ── Internal ────────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
private runScan(entries: KnowledgeEntry[], ctx: AuditContext): AuditIssue[] {
|
|
555
|
+
const issues: AuditIssue[] = [];
|
|
556
|
+
for (const entry of entries) {
|
|
557
|
+
for (const rule of perEntryRules) {
|
|
558
|
+
const detail = rule.detect(entry, ctx);
|
|
559
|
+
if (detail) {
|
|
560
|
+
issues.push({
|
|
561
|
+
rule: rule.name,
|
|
562
|
+
entryId: entry.id,
|
|
563
|
+
title: entry.title,
|
|
564
|
+
severity: rule.severity,
|
|
565
|
+
action: rule.action,
|
|
566
|
+
detail,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return issues;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private applyFixes(issues: AuditIssue[]): number {
|
|
575
|
+
let fixed = 0;
|
|
576
|
+
const processed = new Set<string>();
|
|
577
|
+
|
|
578
|
+
for (const issue of issues) {
|
|
579
|
+
if (processed.has(issue.entryId)) continue;
|
|
580
|
+
|
|
581
|
+
switch (issue.action) {
|
|
582
|
+
case 'archive':
|
|
583
|
+
this.store.update(issue.entryId, { maturity: 'archived' });
|
|
584
|
+
processed.add(issue.entryId);
|
|
585
|
+
fixed++;
|
|
586
|
+
break;
|
|
587
|
+
|
|
588
|
+
case 'demote':
|
|
589
|
+
this.store.update(issue.entryId, { maturity: 'draft' });
|
|
590
|
+
processed.add(issue.entryId);
|
|
591
|
+
fixed++;
|
|
592
|
+
break;
|
|
593
|
+
|
|
594
|
+
case 'flag': {
|
|
595
|
+
const entry = this.store.get(issue.entryId);
|
|
596
|
+
if (entry && !entry.tags.includes('low_quality')) {
|
|
597
|
+
this.store.update(issue.entryId, { tags: [...entry.tags, 'low_quality'] });
|
|
598
|
+
processed.add(issue.entryId);
|
|
599
|
+
fixed++;
|
|
600
|
+
}
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
case 'trim': {
|
|
605
|
+
const entry = this.store.get(issue.entryId);
|
|
606
|
+
if (entry && entry.sourceReferences && entry.sourceReferences.length > MAX_SOURCE_REFS) {
|
|
607
|
+
this.store.update(issue.entryId, {
|
|
608
|
+
sourceReferences: entry.sourceReferences.slice(-MAX_SOURCE_REFS),
|
|
609
|
+
});
|
|
610
|
+
processed.add(issue.entryId);
|
|
611
|
+
fixed++;
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
case 'reject':
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return fixed;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private calculateHealthScore(entries: KnowledgeEntry[], issues: AuditIssue[]): number {
|
|
625
|
+
if (entries.length === 0) return 100;
|
|
626
|
+
|
|
627
|
+
const penalty = issues.reduce((sum, issue) => {
|
|
628
|
+
switch (issue.severity) {
|
|
629
|
+
case 'critical': return sum + 10;
|
|
630
|
+
case 'high': return sum + 5;
|
|
631
|
+
case 'medium': return sum + 2;
|
|
632
|
+
case 'low': return sum + 1;
|
|
633
|
+
default: return sum;
|
|
634
|
+
}
|
|
635
|
+
}, 0);
|
|
636
|
+
|
|
637
|
+
const maxPenalty = entries.length * 10;
|
|
638
|
+
return Math.max(0, 100 - Math.round((penalty / Math.max(maxPenalty, 1)) * 100));
|
|
639
|
+
}
|
|
640
|
+
}
|
package/src/knowledge/import.ts
CHANGED
package/src/knowledge/index.ts
CHANGED
|
@@ -8,9 +8,13 @@ export * from './types';
|
|
|
8
8
|
export { KnowledgeStore } from './store';
|
|
9
9
|
export { KnowledgeQuery } from './query';
|
|
10
10
|
export { KnowledgeLifecycle } from './lifecycle';
|
|
11
|
-
export {
|
|
11
|
+
export type { ConsumptionEvent } from './lifecycle';
|
|
12
|
+
export { KnowledgeIngest, sanitizeExternalContent } from './ingest';
|
|
12
13
|
export { ReferenceTracker } from './reference-tracker';
|
|
13
14
|
export { KnowledgeLinter } from './lint';
|
|
14
15
|
export { ColdStartImporter } from './import';
|
|
15
16
|
export { KnowledgeHealthScorer } from './doctor';
|
|
16
17
|
export { KnowledgeLifecycleHooks } from './lifecycle-hooks';
|
|
18
|
+
export { KnowledgeAudit } from './audit';
|
|
19
|
+
export type { AuditRuleName, AuditAction, AuditIssue, AuditReport, AuditOptions } from './audit';
|
|
20
|
+
export { migrateKnowledgeEntries } from './migration';
|