@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,335 @@
1
+ /**
2
+ * KnowledgeIndexGenerator tests
3
+ *
4
+ * Covers:
5
+ * - Frontmatter extraction (id, type, title, maturity, tags)
6
+ * - Files without frontmatter (best-effort from filename/H1)
7
+ * - Excluded directories (.archive, .snapshots, resolutions)
8
+ * - H2 heading extraction as search terms
9
+ * - Index file format (header + data lines)
10
+ * - Overwrite behavior
11
+ */
12
+
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ import * as os from 'os';
16
+ import { KnowledgeIndexGenerator } from '../index-generator';
17
+
18
+ function makeTmpDir(): string {
19
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'idx-gen-test-'));
20
+ }
21
+
22
+ function writeEntry(dir: string, filename: string, frontmatter: Record<string, unknown> | null, body: string = 'Content here'): void {
23
+ const lines: string[] = [];
24
+ if (frontmatter) {
25
+ lines.push('---');
26
+ for (const [k, v] of Object.entries(frontmatter)) {
27
+ if (Array.isArray(v)) {
28
+ lines.push(`${k}:`);
29
+ for (const item of v) lines.push(` - ${item}`);
30
+ } else {
31
+ lines.push(`${k}: ${JSON.stringify(v)}`);
32
+ }
33
+ }
34
+ lines.push('---');
35
+ lines.push('');
36
+ }
37
+ lines.push(body);
38
+ fs.writeFileSync(path.join(dir, filename), lines.join('\n'));
39
+ }
40
+
41
+ describe('KnowledgeIndexGenerator', () => {
42
+ let tmpDir: string;
43
+
44
+ beforeEach(() => {
45
+ tmpDir = makeTmpDir();
46
+ });
47
+
48
+ afterEach(() => {
49
+ fs.rmSync(tmpDir, { recursive: true, force: true });
50
+ });
51
+
52
+ describe('generateIndexLines', () => {
53
+ it('extracts metadata from frontmatter', () => {
54
+ writeEntry(tmpDir, 'architecture-ARC-011.md', {
55
+ id: 'ARC-011',
56
+ type: 'architecture',
57
+ title: 'JWT认证系统',
58
+ maturity: 'verified',
59
+ tags: ['jwt', 'auth'],
60
+ });
61
+
62
+ const gen = new KnowledgeIndexGenerator(tmpDir);
63
+ const lines = gen.generateIndexLines();
64
+
65
+ expect(lines).toHaveLength(1);
66
+ const fields = lines[0].split('|');
67
+ expect(fields[0]).toBe('architecture-ARC-011.md');
68
+ expect(fields[1]).toBe('ARC-011');
69
+ expect(fields[2]).toBe('architecture');
70
+ expect(fields[3]).toBe('JWT认证系统');
71
+ expect(fields[4]).toBe('verified');
72
+ expect(fields[5]).toBe('jwt;auth');
73
+ });
74
+
75
+ it('includes H2 headings as search terms', () => {
76
+ writeEntry(tmpDir, 'pitfall-001.md', {
77
+ id: 'PIT-001',
78
+ type: 'pitfall',
79
+ title: 'Test Pitfall',
80
+ maturity: 'verified',
81
+ tags: [],
82
+ }, `# Title
83
+
84
+ ## 问题根因
85
+ Some text
86
+
87
+ ## 修复方案
88
+ More text
89
+
90
+ ## 注意事项
91
+ Extra text
92
+ `);
93
+
94
+ const gen = new KnowledgeIndexGenerator(tmpDir);
95
+ const lines = gen.generateIndexLines();
96
+ const terms = lines[0].split('|')[6];
97
+
98
+ expect(terms).toContain('问题根因');
99
+ expect(terms).toContain('修复方案');
100
+ expect(terms).toContain('注意事项');
101
+ });
102
+
103
+ it('handles files without frontmatter via best-effort', () => {
104
+ fs.writeFileSync(
105
+ path.join(tmpDir, 'agent-network-core-concepts.md'),
106
+ '# Agent Network 核心概念\n\n## WorkUnit\nBody\n'
107
+ );
108
+
109
+ const gen = new KnowledgeIndexGenerator(tmpDir);
110
+ const lines = gen.generateIndexLines();
111
+
112
+ expect(lines).toHaveLength(1);
113
+ const fields = lines[0].split('|');
114
+ expect(fields[0]).toBe('agent-network-core-concepts.md');
115
+ // agent-network-* prefix maps to architecture
116
+ expect(fields[2]).toBe('architecture');
117
+ // H2 terms should still be extracted
118
+ expect(fields[6]).toContain('WorkUnit');
119
+ });
120
+
121
+ it('excludes .archive directory', () => {
122
+ fs.mkdirSync(path.join(tmpDir, '.archive'));
123
+ writeEntry(tmpDir, 'top.md', { id: 'T1', type: 'pitfall', title: 'Top', maturity: 'verified', tags: [] });
124
+ writeEntry(tmpDir, '.archive/archived.md', { id: 'A1', type: 'pitfall', title: 'Archived', maturity: 'archived', tags: [] });
125
+
126
+ const gen = new KnowledgeIndexGenerator(tmpDir);
127
+ const lines = gen.generateIndexLines();
128
+
129
+ expect(lines).toHaveLength(1);
130
+ expect(lines[0]).toContain('top.md');
131
+ expect(lines.join('')).not.toContain('archived.md');
132
+ });
133
+
134
+ it('excludes resolutions directory', () => {
135
+ fs.mkdirSync(path.join(tmpDir, 'resolutions'));
136
+ writeEntry(tmpDir, 'top.md', { id: 'T1', type: 'pitfall', title: 'Top', maturity: 'verified', tags: [] });
137
+ writeEntry(tmpDir, 'resolutions/res-001.md', { id: 'R1', type: 'decision', title: 'Res', maturity: 'verified', tags: [] });
138
+
139
+ const gen = new KnowledgeIndexGenerator(tmpDir);
140
+ const lines = gen.generateIndexLines();
141
+
142
+ expect(lines).toHaveLength(1);
143
+ expect(lines.join('')).not.toContain('res-001.md');
144
+ });
145
+
146
+ it('excludes .snapshots directory', () => {
147
+ fs.mkdirSync(path.join(tmpDir, '.snapshots'));
148
+ writeEntry(tmpDir, 'top.md', { id: 'T1', type: 'pitfall', title: 'Top', maturity: 'verified', tags: [] });
149
+ writeEntry(tmpDir, '.snapshots/snap.md', { id: 'S1', type: 'pitfall', title: 'Snap', maturity: 'verified', tags: [] });
150
+
151
+ const gen = new KnowledgeIndexGenerator(tmpDir);
152
+ const lines = gen.generateIndexLines();
153
+
154
+ expect(lines).toHaveLength(1);
155
+ expect(lines.join('')).not.toContain('snap.md');
156
+ });
157
+
158
+ it('excludes _index.md from index', () => {
159
+ writeEntry(tmpDir, '_index.md', { id: 'IDX', type: 'pitfall', title: 'Index', maturity: 'verified', tags: [] });
160
+ writeEntry(tmpDir, 'real.md', { id: 'R1', type: 'pitfall', title: 'Real', maturity: 'verified', tags: [] });
161
+
162
+ const gen = new KnowledgeIndexGenerator(tmpDir);
163
+ const lines = gen.generateIndexLines();
164
+
165
+ expect(lines).toHaveLength(1);
166
+ expect(lines[0]).toContain('real.md');
167
+ expect(lines.join('')).not.toContain('_index.md');
168
+ });
169
+
170
+ it('generates header with stats', () => {
171
+ writeEntry(tmpDir, 'a.md', { id: 'A1', type: 'pitfall', title: 'A', maturity: 'verified', tags: [] });
172
+ writeEntry(tmpDir, 'b.md', { id: 'B1', type: 'architecture', title: 'B', maturity: 'draft', tags: [] });
173
+
174
+ const gen = new KnowledgeIndexGenerator(tmpDir);
175
+ const output = gen.generate();
176
+
177
+ expect(output).toContain('# Knowledge Base Index');
178
+ expect(output).toContain('Total: 2');
179
+ expect(output).toContain('# filename|id|type|title|maturity|tags|terms');
180
+ });
181
+ });
182
+
183
+ describe('regenerate', () => {
184
+ it('writes _index.md to baseDir', () => {
185
+ writeEntry(tmpDir, 'test.md', { id: 'T1', type: 'guideline', title: 'Test', maturity: 'verified', tags: ['tdd'] });
186
+
187
+ const gen = new KnowledgeIndexGenerator(tmpDir);
188
+ gen.regenerate();
189
+
190
+ const indexPath = path.join(tmpDir, '_index.md');
191
+ expect(fs.existsSync(indexPath)).toBe(true);
192
+
193
+ const content = fs.readFileSync(indexPath, 'utf-8');
194
+ expect(content).toContain('test.md');
195
+ expect(content).toContain('T1');
196
+ expect(content).toContain('guideline');
197
+ });
198
+
199
+ it('overwrites existing index', () => {
200
+ fs.writeFileSync(path.join(tmpDir, '_index.md'), 'old content');
201
+ writeEntry(tmpDir, 'new.md', { id: 'N1', type: 'pitfall', title: 'New', maturity: 'verified', tags: [] });
202
+
203
+ const gen = new KnowledgeIndexGenerator(tmpDir);
204
+ gen.regenerate();
205
+
206
+ const content = fs.readFileSync(path.join(tmpDir, '_index.md'), 'utf-8');
207
+ expect(content).toContain('new.md');
208
+ expect(content).not.toContain('old content');
209
+ });
210
+ });
211
+
212
+ describe('edge cases', () => {
213
+ it('handles empty directory', () => {
214
+ const gen = new KnowledgeIndexGenerator(tmpDir);
215
+ const lines = gen.generateIndexLines();
216
+ expect(lines).toHaveLength(0);
217
+ });
218
+
219
+ it('handles subdirectories (arch-patterns, skills)', () => {
220
+ fs.mkdirSync(path.join(tmpDir, 'arch-patterns'));
221
+ writeEntry(tmpDir, 'arch-patterns/event.md', {
222
+ id: 'AP-event', type: 'architecture', title: 'Event Pattern', maturity: 'verified', tags: [],
223
+ });
224
+
225
+ const gen = new KnowledgeIndexGenerator(tmpDir);
226
+ const lines = gen.generateIndexLines();
227
+
228
+ expect(lines).toHaveLength(1);
229
+ expect(lines[0]).toContain('event.md');
230
+ });
231
+
232
+ it('handles missing tags array gracefully', () => {
233
+ writeEntry(tmpDir, 'no-tags.md', {
234
+ id: 'NT1', type: 'pitfall', title: 'No Tags', maturity: 'verified',
235
+ });
236
+
237
+ const gen = new KnowledgeIndexGenerator(tmpDir);
238
+ const lines = gen.generateIndexLines();
239
+
240
+ expect(lines).toHaveLength(1);
241
+ const fields = lines[0].split('|');
242
+ expect(fields[5]).toBe('');
243
+ });
244
+
245
+ it('sanitizes newlines in title to preserve single-line format', () => {
246
+ // Simulates YAML block scalar: title: |\n Line 1\n Line 2
247
+ fs.writeFileSync(
248
+ path.join(tmpDir, 'guideline-GUI-007.md'),
249
+ '---\nid: GUI-007\ntype: guideline\ntitle: |\n Line one\n Line two\nmaturity: archived\ntags: []\n---\nBody\n'
250
+ );
251
+
252
+ const gen = new KnowledgeIndexGenerator(tmpDir);
253
+ const lines = gen.generateIndexLines();
254
+
255
+ expect(lines).toHaveLength(1);
256
+ expect(lines[0]).not.toContain('\n');
257
+ const fields = lines[0].split('|');
258
+ expect(fields).toHaveLength(7);
259
+ expect(fields[3]).toBe('Line one Line two');
260
+ });
261
+
262
+ it('sanitizes pipe characters in fields', () => {
263
+ writeEntry(tmpDir, 'test.md', {
264
+ id: 'T1', type: 'pitfall', title: 'A | B', maturity: 'verified', tags: ['x|y'],
265
+ });
266
+
267
+ const gen = new KnowledgeIndexGenerator(tmpDir);
268
+ const lines = gen.generateIndexLines();
269
+
270
+ expect(lines).toHaveLength(1);
271
+ const fields = lines[0].split('|');
272
+ expect(fields).toHaveLength(7);
273
+ expect(fields[3]).toBe('A / B');
274
+ expect(fields[5]).toBe('x/y');
275
+ });
276
+
277
+ it('detects skill schema (name/description instead of id/type)', () => {
278
+ fs.mkdirSync(path.join(tmpDir, 'skills'), { recursive: true });
279
+ fs.writeFileSync(
280
+ path.join(tmpDir, 'skills', 'proxy.md'),
281
+ '---\nname: proxy\ndescription: "Proxy skill for testing"\ntrigger: manual\nstatus: active\n---\n## Steps\nDo things\n',
282
+ );
283
+
284
+ const gen = new KnowledgeIndexGenerator(tmpDir);
285
+ const lines = gen.generateIndexLines();
286
+
287
+ expect(lines).toHaveLength(1);
288
+ const fields = lines[0].split('|');
289
+ expect(fields[1]).toBe('proxy');
290
+ expect(fields[2]).toBe('skill');
291
+ expect(fields[3]).toContain('Proxy skill');
292
+ });
293
+
294
+ it('infers type from directory (skills/ → skill, arch-patterns/ → architecture)', () => {
295
+ fs.mkdirSync(path.join(tmpDir, 'arch-patterns'), { recursive: true });
296
+ fs.writeFileSync(
297
+ path.join(tmpDir, 'arch-patterns', 'event.md'),
298
+ '# Event Pattern\n\n## Lifecycle\nBody\n',
299
+ );
300
+
301
+ const gen = new KnowledgeIndexGenerator(tmpDir);
302
+ const lines = gen.generateIndexLines();
303
+
304
+ expect(lines).toHaveLength(1);
305
+ const fields = lines[0].split('|');
306
+ expect(fields[2]).toBe('architecture');
307
+ });
308
+
309
+ it('infers pattern type from suffix (*-pattern.md)', () => {
310
+ fs.writeFileSync(
311
+ path.join(tmpDir, 'knowledge-quality-pattern.md'),
312
+ '# Quality Pattern\n\n## Dimensions\nBody\n',
313
+ );
314
+
315
+ const gen = new KnowledgeIndexGenerator(tmpDir);
316
+ const lines = gen.generateIndexLines();
317
+
318
+ expect(lines).toHaveLength(1);
319
+ const fields = lines[0].split('|');
320
+ expect(fields[2]).toBe('pattern');
321
+ });
322
+
323
+ it('skips ghost files named .md', () => {
324
+ fs.mkdirSync(path.join(tmpDir, 'skills'), { recursive: true });
325
+ fs.writeFileSync(path.join(tmpDir, 'skills', '.md'), '# Ghost\n');
326
+ writeEntry(tmpDir, 'real.md', { id: 'R1', type: 'pitfall', title: 'Real', maturity: 'verified', tags: [] });
327
+
328
+ const gen = new KnowledgeIndexGenerator(tmpDir);
329
+ const lines = gen.generateIndexLines();
330
+
331
+ expect(lines).toHaveLength(1);
332
+ expect(lines[0]).toContain('real.md');
333
+ });
334
+ });
335
+ });
@@ -30,11 +30,14 @@ export type AuditRuleName =
30
30
  // D3: 去重有效性
31
31
  | 'title-duplicate'
32
32
  | 'source-refs-bloat'
33
+ | 'fragment-cluster'
33
34
  // D4: 成熟度健康
34
35
  | 'promotion-blocked'
35
36
  | 'orphan-draft'
36
37
  // D5: 新鲜度
37
- | 'stale-entry';
38
+ | 'stale-entry'
39
+ // D2: 领域相关性
40
+ | 'deprecated-domain';
38
41
 
39
42
  export type AuditAction = 'archive' | 'demote' | 'flag' | 'reject' | 'trim';
40
43
 
@@ -96,6 +99,12 @@ const EVENT_NOISE_PATTERNS = [
96
99
  /^\[Session Feature\]\s/,
97
100
  ];
98
101
  const REQUIRED_FRONTMATTER = ['id', 'type', 'title', 'maturity'];
102
+ const SYNTHETIC_REF_PATTERN = /^(search|test-agent|prompt-inject|monitor|analyst|auditor|triage|executor|session|trend|incident):\d{4}-\d{2}-\d{2}/;
103
+
104
+ /** Filter out synthetic references (automated search/ops records, not genuine consumption) */
105
+ function genuineRefs(refs: string[]): string[] {
106
+ return refs.filter(r => !SYNTHETIC_REF_PATTERN.test(r));
107
+ }
99
108
 
100
109
  // ── Per-Entry Rules ───────────────────────────────────────
101
110
 
@@ -222,11 +231,12 @@ const perEntryRules: AuditRule[] = [
222
231
  detect: (entry, ctx) => {
223
232
  if (entry.maturity === 'archived') return null;
224
233
  if (!ctx.allEntries) return null;
234
+ if (!entry.title) return null;
225
235
  const dupes = ctx.allEntries.filter(e =>
226
236
  e.id !== entry.id &&
227
237
  e.maturity !== 'archived' &&
228
238
  e.type === entry.type &&
229
- e.title.toLowerCase().trim() === entry.title.toLowerCase().trim()
239
+ e.title?.toLowerCase().trim() === entry.title.toLowerCase().trim()
230
240
  );
231
241
  if (dupes.length > 0) {
232
242
  return `与 ${dupes.map(e => e.id).join(', ')} 标题重复`;
@@ -245,6 +255,31 @@ const perEntryRules: AuditRule[] = [
245
255
  return null;
246
256
  },
247
257
  },
258
+ {
259
+ name: 'fragment-cluster',
260
+ severity: 'medium',
261
+ action: 'flag',
262
+ detect: (entry, ctx) => {
263
+ if (entry.maturity === 'archived') return null;
264
+ if (!ctx.allEntries || entry.content.trim().length >= 100) return null;
265
+ if (!entry.tags?.length) return null;
266
+
267
+ // Find peers: same type, short body, shared tags ≥2
268
+ const entryTags = new Set(entry.tags);
269
+ const peers = ctx.allEntries.filter(e =>
270
+ e.id !== entry.id &&
271
+ e.maturity !== 'archived' &&
272
+ e.type === entry.type &&
273
+ e.content.trim().length < 100 &&
274
+ e.tags?.filter(t => entryTags.has(t)).length >= 2
275
+ );
276
+
277
+ if (peers.length >= 2) {
278
+ return `碎片集群:与 ${peers.map(e => e.id).join(', ')} 同 type+tags 且 body 均 <100 字符,建议合并`;
279
+ }
280
+ return null;
281
+ },
282
+ },
248
283
 
249
284
  // D4: 成熟度健康
250
285
  {
@@ -267,7 +302,7 @@ const perEntryRules: AuditRule[] = [
267
302
  action: 'flag',
268
303
  detect: (entry) => {
269
304
  if (entry.maturity !== 'draft') return null;
270
- if (entry.contributors.length === 0 && entry.projects.length === 0 && entry.referencedBy.length === 0) {
305
+ if (entry.contributors.length === 0 && entry.projects.length === 0 && genuineRefs(entry.referencedBy).length === 0) {
271
306
  return `draft 无贡献者/项目/引用`;
272
307
  }
273
308
  return null;
@@ -290,6 +325,27 @@ const perEntryRules: AuditRule[] = [
290
325
  return null;
291
326
  },
292
327
  },
328
+
329
+ // D2b: 领域相关性 — 检测已废弃领域的残留条目
330
+ {
331
+ name: 'deprecated-domain',
332
+ severity: 'high',
333
+ action: 'archive',
334
+ detect: (entry) => {
335
+ if (entry.maturity === 'archived') return null;
336
+ const tags = (entry.tags ?? []).map(t => t.toLowerCase());
337
+ // Tag-level: "pipeline" tag is deprecated (Studio Pipeline superseded by Agent Network)
338
+ if (tags.includes('pipeline')) {
339
+ return `标签 "pipeline" 属于已废弃领域(已被 Agent Network 取代)`;
340
+ }
341
+ // Title-level: strong domain-specific terms
342
+ const title = entry.title || '';
343
+ if (/pipeline|管线|GoalExecution|DeployAgent|Integration step/i.test(title)) {
344
+ return `标题引用已废弃领域: "${title}"`;
345
+ }
346
+ return null;
347
+ },
348
+ },
293
349
  ];
294
350
 
295
351
  // ── Audit Engine ──────────────────────────────────────────
@@ -337,7 +393,7 @@ export class KnowledgeAudit {
337
393
  * 全量扫描(日兜底 / 手动模式)
338
394
  */
339
395
  run(options?: { autoFix?: boolean }): AuditReport {
340
- const entries = this.store.list({ excludeArchived: false });
396
+ const entries = this.store.list({ excludeArchived: false }).filter(e => e.title && e.type && e.content != null);
341
397
  const ctx: AuditContext = {
342
398
  shortContentThreshold: this.shortContentThreshold,
343
399
  staleDays: this.staleDays,
@@ -407,12 +463,12 @@ export class KnowledgeAudit {
407
463
  const d1Score = entries.length === 0 ? 100 : Math.max(0, 100 - (d1Issues.length / entries.length) * 100);
408
464
 
409
465
  // D2: 内容质量
410
- const d2Rules: AuditRuleName[] = ['test-data-pollution', 'daily-audit-noise', 'event-noise', 'zero-content-proven', 'short-content', 'maturity-inflation'];
466
+ const d2Rules: AuditRuleName[] = ['test-data-pollution', 'daily-audit-noise', 'event-noise', 'zero-content-proven', 'short-content', 'maturity-inflation', 'deprecated-domain'];
411
467
  const d2Issues = issues.filter(i => d2Rules.includes(i.rule));
412
468
  const d2Score = active.length === 0 ? 100 : Math.max(0, 100 - (d2Issues.length / active.length) * 100);
413
469
 
414
470
  // D3: 去重有效性
415
- const d3Rules: AuditRuleName[] = ['title-duplicate', 'source-refs-bloat'];
471
+ const d3Rules: AuditRuleName[] = ['title-duplicate', 'source-refs-bloat', 'fragment-cluster'];
416
472
  const d3Issues = issues.filter(i => d3Rules.includes(i.rule));
417
473
  const d3Score = active.length === 0 ? 100 : Math.max(0, 100 - (d3Issues.length / active.length) * 100);
418
474
 
@@ -441,10 +497,10 @@ export class KnowledgeAudit {
441
497
  const d5Score = active.length === 0 ? 100 : Math.max(0, 100 - (staleRatio * 100));
442
498
 
443
499
  // D6: 飞轮验证
444
- const withRefs = active.filter(e => e.referencedBy.length > 0).length;
500
+ const withRefs = active.filter(e => genuineRefs(e.referencedBy).length > 0).length;
445
501
  const refCoverage = active.length > 0 ? withRefs / active.length : 0;
446
502
  const avgRefs = active.length > 0
447
- ? active.reduce((sum, e) => sum + e.referencedBy.length, 0) / active.length
503
+ ? active.reduce((sum, e) => sum + genuineRefs(e.referencedBy).length, 0) / active.length
448
504
  : 0;
449
505
 
450
506
  // Consumption hit rate from aggregated stats file (written by MonitorAgent)