@dommaker/harness 0.16.0 → 0.16.1

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 (79) hide show
  1. package/bin/harness.js +71 -0
  2. package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
  3. package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
  4. package/dist/cli/commands/doc-freshness-check.js +366 -0
  5. package/dist/cli/commands/doc-freshness-check.js.map +1 -0
  6. package/dist/cli/commands/index.d.ts +4 -1
  7. package/dist/cli/commands/index.d.ts.map +1 -1
  8. package/dist/cli/commands/index.js +13 -1
  9. package/dist/cli/commands/index.js.map +1 -1
  10. package/dist/cli/commands/knowledge.d.ts +12 -0
  11. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  12. package/dist/cli/commands/knowledge.js +134 -0
  13. package/dist/cli/commands/knowledge.js.map +1 -1
  14. package/dist/cli/commands/release.d.ts +11 -0
  15. package/dist/cli/commands/release.d.ts.map +1 -1
  16. package/dist/cli/commands/release.js +43 -12
  17. package/dist/cli/commands/release.js.map +1 -1
  18. package/dist/cli/commands/sdd.d.ts +8 -0
  19. package/dist/cli/commands/sdd.d.ts.map +1 -0
  20. package/dist/cli/commands/sdd.js +68 -0
  21. package/dist/cli/commands/sdd.js.map +1 -0
  22. package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
  23. package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
  24. package/dist/cli/commands/spec-baseline-check.js +366 -0
  25. package/dist/cli/commands/spec-baseline-check.js.map +1 -0
  26. package/dist/knowledge/audit.d.ts +1 -1
  27. package/dist/knowledge/audit.d.ts.map +1 -1
  28. package/dist/knowledge/audit.js +59 -7
  29. package/dist/knowledge/audit.js.map +1 -1
  30. package/dist/knowledge/index-generator.d.ts +36 -0
  31. package/dist/knowledge/index-generator.d.ts.map +1 -0
  32. package/dist/knowledge/index-generator.js +241 -0
  33. package/dist/knowledge/index-generator.js.map +1 -0
  34. package/dist/knowledge/index.d.ts +2 -0
  35. package/dist/knowledge/index.d.ts.map +1 -1
  36. package/dist/knowledge/index.js +3 -1
  37. package/dist/knowledge/index.js.map +1 -1
  38. package/dist/knowledge/ingest.js +2 -2
  39. package/dist/knowledge/ingest.js.map +1 -1
  40. package/dist/knowledge/lifecycle.d.ts +15 -0
  41. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  42. package/dist/knowledge/lifecycle.js +70 -0
  43. package/dist/knowledge/lifecycle.js.map +1 -1
  44. package/dist/knowledge/lint.js +3 -3
  45. package/dist/knowledge/lint.js.map +1 -1
  46. package/dist/knowledge/primitives/code-structure.d.ts +37 -0
  47. package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
  48. package/dist/knowledge/primitives/code-structure.js +168 -0
  49. package/dist/knowledge/primitives/code-structure.js.map +1 -0
  50. package/dist/knowledge/types.d.ts +3 -1
  51. package/dist/knowledge/types.d.ts.map +1 -1
  52. package/dist/knowledge/types.js.map +1 -1
  53. package/dist/sdd/index-generator.d.ts +22 -0
  54. package/dist/sdd/index-generator.d.ts.map +1 -0
  55. package/dist/sdd/index-generator.js +118 -0
  56. package/dist/sdd/index-generator.js.map +1 -0
  57. package/package.json +1 -1
  58. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
  59. package/src/cli/commands/__tests__/knowledge.test.ts +69 -1
  60. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
  61. package/src/cli/commands/doc-freshness-check.ts +397 -0
  62. package/src/cli/commands/index.ts +4 -1
  63. package/src/cli/commands/knowledge.ts +150 -0
  64. package/src/cli/commands/release.ts +48 -12
  65. package/src/cli/commands/sdd.ts +44 -0
  66. package/src/cli/commands/spec-baseline-check.ts +384 -0
  67. package/src/knowledge/__tests__/audit.test.ts +56 -0
  68. package/src/knowledge/__tests__/index-generator.test.ts +335 -0
  69. package/src/knowledge/audit.ts +64 -8
  70. package/src/knowledge/index-generator.ts +233 -0
  71. package/src/knowledge/index.ts +2 -0
  72. package/src/knowledge/ingest.ts +2 -2
  73. package/src/knowledge/lifecycle.ts +71 -0
  74. package/src/knowledge/lint.ts +3 -3
  75. package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
  76. package/src/knowledge/primitives/code-structure.ts +161 -0
  77. package/src/knowledge/types.ts +4 -1
  78. package/src/sdd/__tests__/index-generator.test.ts +150 -0
  79. package/src/sdd/index-generator.ts +109 -0
@@ -0,0 +1,397 @@
1
+ /**
2
+ * harness doc-freshness-check 命令
3
+ *
4
+ * 从文档中提取可验证的声明(claims),与代码/文件系统对照,输出差异。
5
+ * 纯代码操作,零 LLM 调用。
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+
12
+ export interface DocFreshnessCheckOptions {
13
+ /** 输出 JSON 格式 */
14
+ format?: 'json' | 'table';
15
+ /** 变更文件列表(逗号分隔),用于聚焦检查 */
16
+ changedFiles?: string;
17
+ /** 项目路径 */
18
+ projectPath?: string;
19
+ }
20
+
21
+ /** 声明类型 */
22
+ export type ClaimType = 'numeric' | 'list' | 'status' | 'narrative';
23
+
24
+ /** 单条声明 */
25
+ export interface Claim {
26
+ /** 原始文本片段 */
27
+ text: string;
28
+ /** 声明类型 */
29
+ type: ClaimType;
30
+ /** 行号 */
31
+ line: number;
32
+ }
33
+
34
+ /** 验证结果 */
35
+ export interface ClaimResult {
36
+ /** 原始声明 */
37
+ claim: string;
38
+ /** 声明类型 */
39
+ type: ClaimType;
40
+ /** 文档中的期望值 */
41
+ expected: string;
42
+ /** 实际值 */
43
+ actual: string;
44
+ /** 来源文件 */
45
+ file: string;
46
+ /** 是否匹配 */
47
+ match: boolean;
48
+ }
49
+
50
+ // ============================================
51
+ // Claim 提取器
52
+ // ============================================
53
+
54
+ /** 数字声明:N 个/条/项 + 名词 */
55
+ const NUMERIC_PATTERNS = [
56
+ /(\d+)\s*个\s*(Agent|角色|能力|子系统|模块|端点|命令|组件|层级|阶段|步骤|断点|问题|维度|门|规则|约束)/g,
57
+ /(\d+)\s*条\s*(知识|规则|约束|命令|铁律|指南|提示|断点|问题|结果|记录)/g,
58
+ /(\d+)\s*项\s*(检查|任务|功能|变更|修复|优化|指标|配置)/g,
59
+ /(\d+)\s*(?:大|层)\s*(能力|约束|架构|层级)/g,
60
+ /(\d+)\s*个\s*(?:CLI\s*)?subcommand/gi,
61
+ ];
62
+
63
+ /** 列表声明:A, B, C, ... (至少 3 项) */
64
+ const LIST_PATTERN = /[A-Z][a-zA-Z]+(?:[,,、]\s*[A-Z][a-zA-Z]+){2,}/g;
65
+
66
+ /** 状态标记 */
67
+ const STATUS_PATTERNS = [
68
+ /✅\s*(已完成|已实现|已修复|已部署|已连通|已验证)/g,
69
+ /❌\s*(缺失|未实现|未完成|失败)/g,
70
+ /\bDEPRECATED\b/g,
71
+ /\bTODO\b/g,
72
+ /待(开发|实现|修复|完善|验证)/g,
73
+ /已(废弃|弃用|移除|删除)/g,
74
+ ];
75
+
76
+ /**
77
+ * 从 markdown 内容提取 claims
78
+ */
79
+ export function extractClaims(content: string, filePath: string): ClaimResult[] {
80
+ const lines = content.split('\n');
81
+ const results: ClaimResult[] = [];
82
+
83
+ for (let i = 0; i < lines.length; i++) {
84
+ const line = lines[i];
85
+
86
+ // 数字声明
87
+ for (const pattern of NUMERIC_PATTERNS) {
88
+ pattern.lastIndex = 0;
89
+ let match;
90
+ while ((match = pattern.exec(line)) !== null) {
91
+ const fullMatch = match[0];
92
+ const number = match[1];
93
+ results.push({
94
+ claim: fullMatch,
95
+ type: 'numeric',
96
+ expected: number,
97
+ actual: '', // 待验证
98
+ file: filePath,
99
+ match: false,
100
+ });
101
+ }
102
+ }
103
+
104
+ // 列表声明
105
+ const listMatches = line.match(LIST_PATTERN);
106
+ if (listMatches) {
107
+ for (const m of listMatches) {
108
+ const items = m.split(/[,,、]/).map(s => s.trim()).filter(Boolean);
109
+ if (items.length >= 3) {
110
+ results.push({
111
+ claim: m,
112
+ type: 'list',
113
+ expected: items.join(', '),
114
+ actual: '', // 待验证
115
+ file: filePath,
116
+ match: false,
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+ // 状态标记
123
+ for (const pattern of STATUS_PATTERNS) {
124
+ pattern.lastIndex = 0;
125
+ let match;
126
+ while ((match = pattern.exec(line)) !== null) {
127
+ results.push({
128
+ claim: match[0],
129
+ type: 'status',
130
+ expected: match[0],
131
+ actual: '', // 待验证
132
+ file: filePath,
133
+ match: false,
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ return results;
140
+ }
141
+
142
+ // ============================================
143
+ // 验证器
144
+ // ============================================
145
+
146
+ /**
147
+ * 在项目目录中 grep 搜索匹配内容,返回匹配行数
148
+ */
149
+ function countGrepMatches(pattern: string, projectPath: string, fileGlob?: string): number {
150
+ const searchDir = projectPath || process.cwd();
151
+ let count = 0;
152
+
153
+ function walkDir(dir: string) {
154
+ let entries: fs.Dirent[];
155
+ try {
156
+ entries = fs.readdirSync(dir, { withFileTypes: true });
157
+ } catch {
158
+ return;
159
+ }
160
+
161
+ for (const entry of entries) {
162
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist') continue;
163
+
164
+ const fullPath = path.join(dir, entry.name);
165
+ if (entry.isDirectory()) {
166
+ walkDir(fullPath);
167
+ } else if (entry.isFile()) {
168
+ if (fileGlob && !matchGlob(entry.name, fileGlob)) continue;
169
+ try {
170
+ const content = fs.readFileSync(fullPath, 'utf-8');
171
+ const regex = new RegExp(pattern, 'gi');
172
+ const matches = content.match(regex);
173
+ if (matches) count += matches.length;
174
+ } catch {
175
+ // skip unreadable files
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ walkDir(searchDir);
182
+ return count;
183
+ }
184
+
185
+ /**
186
+ * 简单 glob 匹配(支持 *.ext 和 *)
187
+ */
188
+ function matchGlob(filename: string, glob: string): boolean {
189
+ if (glob === '*') return true;
190
+ if (glob.startsWith('*.')) {
191
+ const ext = glob.slice(1);
192
+ return filename.endsWith(ext);
193
+ }
194
+ return filename === glob;
195
+ }
196
+
197
+ /**
198
+ * 验证数字声明:计数相关文件/代码模式
199
+ */
200
+ function verifyNumericClaim(claim: ClaimResult, projectPath: string): ClaimResult {
201
+ const expected = parseInt(claim.expected, 10);
202
+
203
+ // 从声明文本推断搜索模式
204
+ let searchPattern = '';
205
+ let fileGlob = '*.ts';
206
+
207
+ if (claim.claim.includes('Agent')) {
208
+ searchPattern = 'export (class|interface|function) \\w*Agent';
209
+ } else if (claim.claim.includes('命令') || claim.claim.includes('subcommand')) {
210
+ fileGlob = '*.ts';
211
+ searchPattern = "program\\.command\\(";
212
+ } else if (claim.claim.includes('铁律') || claim.claim.includes('约束') || claim.claim.includes('规则')) {
213
+ searchPattern = "severity:\\s*['\"]?error";
214
+ fileGlob = '*.ts';
215
+ } else if (claim.claim.includes('指南')) {
216
+ searchPattern = "severity:\\s*['\"]?warning";
217
+ fileGlob = '*.ts';
218
+ } else if (claim.claim.includes('端点')) {
219
+ searchPattern = '(router|app)\\.(get|post|put|delete|patch)\\(';
220
+ fileGlob = '*.ts';
221
+ } else {
222
+ // 无法自动验证的数字声明
223
+ return { ...claim, actual: 'N/A (cannot auto-verify)', match: true };
224
+ }
225
+
226
+ const actual = countGrepMatches(searchPattern, projectPath, fileGlob);
227
+
228
+ return {
229
+ ...claim,
230
+ actual: String(actual),
231
+ match: Math.abs(actual - expected) <= Math.max(1, Math.floor(expected * 0.1)), // 10% tolerance
232
+ };
233
+ }
234
+
235
+ /**
236
+ * 验证列表声明:检查列表中的每一项是否存在于代码中
237
+ */
238
+ function verifyListClaim(claim: ClaimResult, projectPath: string): ClaimResult {
239
+ const items = claim.expected.split(',').map(s => s.trim());
240
+ const found: string[] = [];
241
+ const missing: string[] = [];
242
+
243
+ for (const item of items) {
244
+ const count = countGrepMatches(item, projectPath);
245
+ if (count > 0) {
246
+ found.push(item);
247
+ } else {
248
+ missing.push(item);
249
+ }
250
+ }
251
+
252
+ return {
253
+ ...claim,
254
+ actual: missing.length === 0
255
+ ? `all ${items.length} found`
256
+ : `missing: ${missing.join(', ')}`,
257
+ match: missing.length === 0,
258
+ };
259
+ }
260
+
261
+ /**
262
+ * 验证状态声明:检查标记是否与代码/文件一致
263
+ */
264
+ function verifyStatusClaim(claim: ClaimResult, projectPath: string): ClaimResult {
265
+ // 状态声明的验证较复杂,标记为需要人工确认
266
+ return {
267
+ ...claim,
268
+ actual: 'needs manual verification',
269
+ match: true, // 不阻断
270
+ };
271
+ }
272
+
273
+ /**
274
+ * 验证所有 claims
275
+ */
276
+ export function verifyClaims(
277
+ claims: ClaimResult[],
278
+ projectPath: string,
279
+ ): ClaimResult[] {
280
+ return claims.map(claim => {
281
+ switch (claim.type) {
282
+ case 'numeric':
283
+ return verifyNumericClaim(claim, projectPath);
284
+ case 'list':
285
+ return verifyListClaim(claim, projectPath);
286
+ case 'status':
287
+ return verifyStatusClaim(claim, projectPath);
288
+ default:
289
+ return { ...claim, actual: 'N/A', match: true };
290
+ }
291
+ });
292
+ }
293
+
294
+ // ============================================
295
+ // 输出格式化
296
+ // ============================================
297
+
298
+ function formatTable(results: ClaimResult[]): string {
299
+ if (results.length === 0) return chalk.yellow('未发现可验证的声明');
300
+
301
+ const lines: string[] = [];
302
+ lines.push(chalk.blue(`文档新鲜度检查 (${results.length} 条声明)\n`));
303
+
304
+ // 统计
305
+ const mismatches = results.filter(r => !r.match);
306
+ lines.push(chalk.bold(` 匹配: ${results.length - mismatches.length}/${results.length}`));
307
+ if (mismatches.length > 0) {
308
+ lines.push(chalk.red(` 不匹配: ${mismatches.length}\n`));
309
+ } else {
310
+ lines.push(chalk.green(' 全部匹配\n'));
311
+ }
312
+
313
+ // 按类型分组
314
+ const byType: Record<string, ClaimResult[]> = {};
315
+ for (const r of results) {
316
+ if (!byType[r.type]) byType[r.type] = [];
317
+ byType[r.type].push(r);
318
+ }
319
+
320
+ const typeLabels: Record<string, string> = {
321
+ numeric: '数字声明',
322
+ list: '列表声明',
323
+ status: '状态标记',
324
+ narrative: '叙述声明',
325
+ };
326
+
327
+ for (const [type, items] of Object.entries(byType)) {
328
+ lines.push(chalk.bold(` ${typeLabels[type] || type} (${items.length}):`));
329
+ for (const item of items) {
330
+ const icon = item.match ? chalk.green('✓') : chalk.red('✗');
331
+ lines.push(` ${icon} ${item.claim}`);
332
+ if (!item.match) {
333
+ lines.push(` 期望: ${item.expected}`);
334
+ lines.push(` 实际: ${chalk.yellow(item.actual)}`);
335
+ }
336
+ }
337
+ lines.push('');
338
+ }
339
+
340
+ return lines.join('\n');
341
+ }
342
+
343
+ // ============================================
344
+ // 主入口
345
+ // ============================================
346
+
347
+ /**
348
+ * doc-freshness-check CLI 命令
349
+ */
350
+ export async function docFreshnessCheck(
351
+ docPath: string,
352
+ options: DocFreshnessCheckOptions = {},
353
+ ): Promise<void> {
354
+ // 验证文件存在
355
+ const resolvedPath = path.resolve(docPath);
356
+ if (!fs.existsSync(resolvedPath)) {
357
+ console.error(chalk.red(`文件不存在: ${resolvedPath}`));
358
+ process.exitCode = 1;
359
+ return;
360
+ }
361
+
362
+ if (!resolvedPath.endsWith('.md')) {
363
+ console.error(chalk.yellow(`警告: 文件不是 .md 格式,可能无法正确解析`));
364
+ }
365
+
366
+ const projectPath = options.projectPath || path.dirname(resolvedPath);
367
+
368
+ // 读取文档
369
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
370
+
371
+ // 提取声明
372
+ let claims = extractClaims(content, resolvedPath);
373
+
374
+ // 如果指定了变更文件,只验证与变更文件相关的声明
375
+ if (options.changedFiles) {
376
+ const changed = options.changedFiles.split(',').map(s => s.trim());
377
+ // 过滤:保留所有 claims,但只验证与变更文件相关的
378
+ // 实际实现中可以根据声明内容推断相关文件
379
+ void changed; // 预留
380
+ }
381
+
382
+ // 验证
383
+ const results = verifyClaims(claims, projectPath);
384
+
385
+ // 输出
386
+ if (options.format === 'json') {
387
+ console.log(JSON.stringify(results, null, 2));
388
+ } else {
389
+ console.log(formatTable(results));
390
+ }
391
+
392
+ // 有不匹配时 exit 1
393
+ const mismatches = results.filter(r => !r.match);
394
+ if (mismatches.length > 0) {
395
+ process.exitCode = 1;
396
+ }
397
+ }
@@ -17,10 +17,13 @@ export { contract, validateSchema, type ContractOptions } from './contract';
17
17
  export { review, reviewStatus, type ReviewOptions } from './review';
18
18
  export { executeCommand, type CommandCheckOptions } from './command';
19
19
  export { syncDocs, type SyncDocsOptions } from './sync-docs';
20
- export { knowledgeList, knowledgeSearch, knowledgeImport, knowledgeDecay, knowledgeStats, knowledgeUpsert, knowledgeSyncStatus, knowledgeSyncRag, knowledgeAudit, knowledgeSnapshot, knowledgeMigrate, type KnowledgeOptions, type KnowledgeUpsertOptions } from './knowledge';
20
+ export { knowledgeList, knowledgeSearch, knowledgeImport, knowledgeDecay, knowledgeStats, knowledgeUpsert, knowledgeSyncStatus, knowledgeSyncRag, knowledgeAudit, knowledgeSnapshot, knowledgeMigrate, knowledgeHealth, knowledgeIndex, type KnowledgeOptions, type KnowledgeUpsertOptions } from './knowledge';
21
21
  export { failureList, failureStats, failureClear, type FailureOptions } from './failure';
22
22
  export { postevalPlan, type PostEvalPlanOptions } from './posteval-plan';
23
23
  export { release, type ReleaseOptions } from './release';
24
24
  export { analyzeSessions, type AnalyzeSessionsOptions } from './analyze-sessions';
25
25
  export { updateUserModel, type UpdateUserModelOptions } from './update-user-model';
26
26
  export { constraints, getConstraintsMeta, type ConstraintsMeta } from './constraints';
27
+ export { docFreshnessCheck, extractClaims, verifyClaims, type DocFreshnessCheckOptions, type ClaimResult, type ClaimType } from './doc-freshness-check';
28
+ export { specBaselineCheck, extractBaselineSection, type SpecBaselineCheckOptions, type PrerequisiteResult } from './spec-baseline-check';
29
+ export { sddIndex, type SDDOptions } from './sdd';
@@ -14,6 +14,7 @@ import { KnowledgeLifecycle } from '../../knowledge/lifecycle';
14
14
  import { ColdStartImporter } from '../../knowledge/import';
15
15
  import { KnowledgeAudit } from '../../knowledge/audit';
16
16
  import { migrateKnowledgeEntries } from '../../knowledge/migration';
17
+ import { KnowledgeIndexGenerator } from '../../knowledge/index-generator';
17
18
  import type { KnowledgeType, MaturityLevel, QueryFilter } from '../../knowledge/types';
18
19
 
19
20
  export interface KnowledgeOptions {
@@ -413,6 +414,106 @@ export async function knowledgeSyncStatus(options: KnowledgeOptions): Promise<vo
413
414
  }
414
415
  }
415
416
 
417
+ /**
418
+ * 飞轮健康检查 — 零 token 检测知识飞轮数据流状态
419
+ */
420
+ export async function knowledgeHealth(options: KnowledgeOptions & { dir?: string }): Promise<void> {
421
+ const baseDir = options.dir || getKnowledgeDir(options.projectPath);
422
+ const store = new KnowledgeStore({ baseDir });
423
+ const entries = store.list({ excludeArchived: true });
424
+
425
+ const issues: Array<{ severity: 'error' | 'warn' | 'info'; entry: string; detail: string }> = [];
426
+
427
+ // D1: 引用密度检查(低引用 = 可能孤立)
428
+ let lowRefEntries = 0;
429
+ for (const entry of entries) {
430
+ if (entry.referencedBy.length === 0 && entry.maturity === 'verified') {
431
+ lowRefEntries++;
432
+ issues.push({ severity: 'info', entry: entry.id, detail: `verified 条目零引用(可能孤立)` });
433
+ }
434
+ }
435
+
436
+ // D2: 新鲜度检查
437
+ const now = Date.now();
438
+ const staleThreshold = 90 * 24 * 60 * 60 * 1000; // 90 days
439
+ let staleEntries = 0;
440
+ for (const entry of entries) {
441
+ const created = new Date(entry.created).getTime();
442
+ if (now - created > staleThreshold && entry.maturity === 'draft') {
443
+ staleEntries++;
444
+ issues.push({ severity: 'warn', entry: entry.id, detail: `draft 超过 90 天未推进` });
445
+ }
446
+ }
447
+
448
+ // D3: 消费数据检查
449
+ let consumptionData = false;
450
+ const statsPath = path.join(baseDir, '.consumption-stats.json');
451
+ if (fs.existsSync(statsPath)) {
452
+ consumptionData = true;
453
+ } else {
454
+ issues.push({ severity: 'info', entry: '-', detail: `消费追踪数据不存在(${statsPath})` });
455
+ }
456
+
457
+ // D4: 飞轮指标
458
+ const withRefs = entries.filter(e => e.referencedBy.length > 0).length;
459
+ const refCoverage = entries.length > 0 ? Math.round(withRefs / entries.length * 100) : 0;
460
+ const avgRefs = entries.length > 0
461
+ ? Math.round(entries.reduce((sum, e) => sum + e.referencedBy.length, 0) / entries.length * 10) / 10
462
+ : 0;
463
+
464
+ // 健康分计算
465
+ const totalIssues = issues.filter(i => i.severity === 'error').length * 3
466
+ + issues.filter(i => i.severity === 'warn').length * 1;
467
+ const healthScore = Math.max(0, 100 - totalIssues);
468
+
469
+ if (options.json) {
470
+ console.log(JSON.stringify({
471
+ healthScore,
472
+ summary: {
473
+ total: entries.length,
474
+ lowRefEntries,
475
+ staleEntries,
476
+ consumptionData,
477
+ refCoverage,
478
+ avgRefs,
479
+ },
480
+ issues: issues.slice(0, 50),
481
+ }, null, 2));
482
+ return;
483
+ }
484
+
485
+ console.log(chalk.blue('🏥 飞轮健康检查\n'));
486
+ console.log(chalk.bold(` 健康分: ${healthScore >= 80 ? chalk.green(healthScore) : healthScore >= 60 ? chalk.yellow(healthScore) : chalk.red(healthScore)}/100`));
487
+ console.log(chalk.bold(` 活跃条目: ${entries.length}`));
488
+ console.log();
489
+
490
+ console.log(chalk.bold(' 数据流状态:'));
491
+ console.log(` 引用密度: ${lowRefEntries === 0 ? chalk.green('✓') : chalk.yellow(`⚠ ${lowRefEntries} 个零引用 verified`)}`);
492
+ console.log(` 新鲜度: ${staleEntries === 0 ? chalk.green('✓') : chalk.yellow(`⚠ ${staleEntries} 个过期 draft`)}`);
493
+ console.log(` 消费追踪: ${consumptionData ? chalk.green('✓') : chalk.gray('○ 未启用')}`);
494
+ console.log();
495
+
496
+ console.log(chalk.bold(' 飞轮指标:'));
497
+ console.log(` 引用覆盖: ${refCoverage}%`);
498
+ console.log(` 平均引用: ${avgRefs}`);
499
+ console.log();
500
+
501
+ if (issues.length > 0) {
502
+ console.log(chalk.bold(` 问题 (前 20):`));
503
+ for (const issue of issues.slice(0, 20)) {
504
+ const sevColor = issue.severity === 'error' ? chalk.red
505
+ : issue.severity === 'warn' ? chalk.yellow
506
+ : chalk.gray;
507
+ console.log(` ${sevColor(`[${issue.severity}]`)} ${issue.entry}: ${issue.detail}`);
508
+ }
509
+ if (issues.length > 20) {
510
+ console.log(chalk.gray(` ... 还有 ${issues.length - 20} 条`));
511
+ }
512
+ } else {
513
+ console.log(chalk.green(' ✓ 无问题'));
514
+ }
515
+ }
516
+
416
517
  function getKnowledgeDir(projectPath?: string): string {
417
518
  if (projectPath) return `${projectPath}/.harness/knowledge`;
418
519
  if (process.env.KNOWLEDGE_BASE_DIR) return process.env.KNOWLEDGE_BASE_DIR;
@@ -482,6 +583,7 @@ export async function knowledgeAudit(options: KnowledgeOptions & {
482
583
  'maturity-inflation': '成熟度虚高',
483
584
  'title-duplicate': '标题重复',
484
585
  'source-refs-bloat': 'sourceReferences 膨胀',
586
+ 'fragment-cluster': '碎片集群',
485
587
  'promotion-blocked': 'promotion 受阻',
486
588
  'orphan-draft': '孤儿 draft',
487
589
  'stale-entry': '过期条目',
@@ -511,6 +613,14 @@ export async function knowledgeAudit(options: KnowledgeOptions & {
511
613
  if (!options.fix && report.issues.length > 0) {
512
614
  console.log(chalk.yellow(`\n 使用 --fix 自动修复`));
513
615
  }
616
+
617
+ // Auto-rebuild index after audit (files may have changed)
618
+ const baseDir = options.dir || getKnowledgeDir(options.projectPath);
619
+ const idxGen = new KnowledgeIndexGenerator(baseDir);
620
+ idxGen.regenerate();
621
+ if (!options.json) {
622
+ console.log(chalk.gray(` 📇 索引已重建`));
623
+ }
514
624
  }
515
625
 
516
626
  /**
@@ -560,3 +670,43 @@ export function knowledgeMigrate(options: KnowledgeOptions & { dir?: string }):
560
670
  console.log(chalk.green(`\n✅ 迁移完成`));
561
671
  }
562
672
  }
673
+
674
+ /**
675
+ * 知识库索引重建 — 生成 _index.md 供 Agent grep 使用,同时同步 index.json
676
+ */
677
+ export function knowledgeIndex(options: KnowledgeOptions & { dir?: string }): void {
678
+ const baseDir = options.dir || getKnowledgeDir(options.projectPath);
679
+ const gen = new KnowledgeIndexGenerator(baseDir);
680
+
681
+ const beforeSize = (() => {
682
+ const indexPath = path.join(baseDir, '_index.md');
683
+ return fs.existsSync(indexPath) ? fs.statSync(indexPath).size : 0;
684
+ })();
685
+
686
+ const output = gen.regenerate();
687
+
688
+ // Sync index.json from disk files (removes ghost entries from manual mv/rm)
689
+ const store = new KnowledgeStore({ baseDir });
690
+ store.rebuildIndex();
691
+
692
+ const afterSize = Buffer.byteLength(output, 'utf-8');
693
+ const lineCount = output.split('\n').filter(l => !l.startsWith('#')).length;
694
+
695
+ if (options.json) {
696
+ console.log(JSON.stringify({
697
+ path: path.join(baseDir, '_index.md'),
698
+ entries: lineCount,
699
+ size: afterSize,
700
+ previousSize: beforeSize,
701
+ }));
702
+ return;
703
+ }
704
+
705
+ console.log(chalk.blue(`📇 索引已重建`));
706
+ console.log(chalk.gray(` 路径: ${path.join(baseDir, '_index.md')}`));
707
+ console.log(chalk.green(` 条目: ${lineCount}`));
708
+ console.log(chalk.green(` 大小: ${(afterSize / 1024).toFixed(1)} KB`));
709
+ if (beforeSize > 0) {
710
+ console.log(chalk.gray(` 旧大小: ${(beforeSize / 1024).toFixed(1)} KB`));
711
+ }
712
+ }