@dommaker/harness 0.14.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 (168) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bin/harness.js +71 -0
  3. package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
  4. package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
  5. package/dist/cli/commands/doc-freshness-check.js +366 -0
  6. package/dist/cli/commands/doc-freshness-check.js.map +1 -0
  7. package/dist/cli/commands/index.d.ts +4 -1
  8. package/dist/cli/commands/index.d.ts.map +1 -1
  9. package/dist/cli/commands/index.js +13 -1
  10. package/dist/cli/commands/index.js.map +1 -1
  11. package/dist/cli/commands/knowledge.d.ts +12 -0
  12. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  13. package/dist/cli/commands/knowledge.js +140 -6
  14. package/dist/cli/commands/knowledge.js.map +1 -1
  15. package/dist/cli/commands/release.d.ts +11 -0
  16. package/dist/cli/commands/release.d.ts.map +1 -1
  17. package/dist/cli/commands/release.js +80 -12
  18. package/dist/cli/commands/release.js.map +1 -1
  19. package/dist/cli/commands/sdd.d.ts +8 -0
  20. package/dist/cli/commands/sdd.d.ts.map +1 -0
  21. package/dist/cli/commands/sdd.js +68 -0
  22. package/dist/cli/commands/sdd.js.map +1 -0
  23. package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
  24. package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
  25. package/dist/cli/commands/spec-baseline-check.js +366 -0
  26. package/dist/cli/commands/spec-baseline-check.js.map +1 -0
  27. package/dist/core/constraints/checker.js +1 -1
  28. package/dist/core/constraints/checker.js.map +1 -1
  29. package/dist/core/constraints/definitions.d.ts.map +1 -1
  30. package/dist/core/constraints/definitions.js +30 -24
  31. package/dist/core/constraints/definitions.js.map +1 -1
  32. package/dist/knowledge/audit.d.ts +1 -1
  33. package/dist/knowledge/audit.d.ts.map +1 -1
  34. package/dist/knowledge/audit.js +60 -8
  35. package/dist/knowledge/audit.js.map +1 -1
  36. package/dist/knowledge/doctor.d.ts +1 -1
  37. package/dist/knowledge/doctor.d.ts.map +1 -1
  38. package/dist/knowledge/import.d.ts +1 -1
  39. package/dist/knowledge/import.d.ts.map +1 -1
  40. package/dist/knowledge/index-generator.d.ts +36 -0
  41. package/dist/knowledge/index-generator.d.ts.map +1 -0
  42. package/dist/knowledge/index-generator.js +241 -0
  43. package/dist/knowledge/index-generator.js.map +1 -0
  44. package/dist/knowledge/index.d.ts +4 -1
  45. package/dist/knowledge/index.d.ts.map +1 -1
  46. package/dist/knowledge/index.js +4 -2
  47. package/dist/knowledge/index.js.map +1 -1
  48. package/dist/knowledge/ingest.d.ts +1 -1
  49. package/dist/knowledge/ingest.d.ts.map +1 -1
  50. package/dist/knowledge/ingest.js +2 -2
  51. package/dist/knowledge/ingest.js.map +1 -1
  52. package/dist/knowledge/lifecycle-hooks.d.ts +1 -1
  53. package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
  54. package/dist/knowledge/lifecycle.d.ts +16 -1
  55. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  56. package/dist/knowledge/lifecycle.js +70 -0
  57. package/dist/knowledge/lifecycle.js.map +1 -1
  58. package/dist/knowledge/lint.d.ts +1 -1
  59. package/dist/knowledge/lint.d.ts.map +1 -1
  60. package/dist/knowledge/lint.js +4 -4
  61. package/dist/knowledge/lint.js.map +1 -1
  62. package/dist/knowledge/primitives/code-structure.d.ts +37 -0
  63. package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
  64. package/dist/knowledge/primitives/code-structure.js +168 -0
  65. package/dist/knowledge/primitives/code-structure.js.map +1 -0
  66. package/dist/knowledge/query.d.ts +1 -1
  67. package/dist/knowledge/query.d.ts.map +1 -1
  68. package/dist/knowledge/reference-tracker.d.ts +1 -1
  69. package/dist/knowledge/reference-tracker.d.ts.map +1 -1
  70. package/dist/knowledge/store.d.ts +29 -2
  71. package/dist/knowledge/store.d.ts.map +1 -1
  72. package/dist/knowledge/store.js +10 -5
  73. package/dist/knowledge/store.js.map +1 -1
  74. package/dist/knowledge/types.d.ts +17 -1
  75. package/dist/knowledge/types.d.ts.map +1 -1
  76. package/dist/knowledge/types.js.map +1 -1
  77. package/dist/monitoring/knowledge-evolver.d.ts +1 -1
  78. package/dist/monitoring/knowledge-evolver.d.ts.map +1 -1
  79. package/dist/sdd/index-generator.d.ts +22 -0
  80. package/dist/sdd/index-generator.d.ts.map +1 -0
  81. package/dist/sdd/index-generator.js +118 -0
  82. package/dist/sdd/index-generator.js.map +1 -0
  83. package/dist/types/constraint.d.ts +1 -1
  84. package/dist/types/constraint.d.ts.map +1 -1
  85. package/package.json +1 -1
  86. package/src/__tests__/checker.test.ts +4 -4
  87. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
  88. package/src/cli/commands/__tests__/knowledge.test.ts +70 -2
  89. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
  90. package/src/cli/commands/doc-freshness-check.ts +397 -0
  91. package/src/cli/commands/index.ts +4 -1
  92. package/src/cli/commands/knowledge.ts +151 -1
  93. package/src/cli/commands/release.ts +86 -12
  94. package/src/cli/commands/sdd.ts +44 -0
  95. package/src/cli/commands/spec-baseline-check.ts +384 -0
  96. package/src/context/__tests__/knowledge-injector.test.ts +1 -1
  97. package/src/core/constraints/checker.ts +1 -1
  98. package/src/core/constraints/definitions.ts +31 -25
  99. package/src/knowledge/__tests__/audit.test.ts +57 -1
  100. package/src/knowledge/__tests__/doctor.test.ts +1 -1
  101. package/src/knowledge/__tests__/import.test.ts +1 -1
  102. package/src/knowledge/__tests__/index-generator.test.ts +335 -0
  103. package/src/knowledge/__tests__/ingest.test.ts +1 -1
  104. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +1 -1
  105. package/src/knowledge/__tests__/lifecycle.test.ts +1 -1
  106. package/src/knowledge/__tests__/lint.test.ts +1 -1
  107. package/src/knowledge/__tests__/query.test.ts +1 -1
  108. package/src/knowledge/__tests__/reference-tracker.test.ts +1 -1
  109. package/src/knowledge/__tests__/store.test.ts +1 -1
  110. package/src/knowledge/audit.ts +67 -10
  111. package/src/knowledge/doctor.ts +1 -1
  112. package/src/knowledge/import.ts +1 -1
  113. package/src/knowledge/index-generator.ts +233 -0
  114. package/src/knowledge/index.ts +4 -1
  115. package/src/knowledge/ingest.ts +3 -3
  116. package/src/knowledge/lifecycle-hooks.ts +1 -1
  117. package/src/knowledge/lifecycle.ts +72 -1
  118. package/src/knowledge/lint.ts +5 -5
  119. package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
  120. package/src/knowledge/primitives/code-structure.ts +161 -0
  121. package/src/knowledge/query.ts +1 -1
  122. package/src/knowledge/reference-tracker.ts +1 -1
  123. package/src/knowledge/store.ts +27 -4
  124. package/src/knowledge/types.ts +21 -1
  125. package/src/monitoring/knowledge-evolver.ts +1 -1
  126. package/src/sdd/__tests__/index-generator.test.ts +150 -0
  127. package/src/sdd/index-generator.ts +109 -0
  128. package/src/types/constraint.ts +1 -1
  129. package/dist/cli/commands/diagnose.d.ts +0 -16
  130. package/dist/cli/commands/diagnose.d.ts.map +0 -1
  131. package/dist/cli/commands/diagnose.js +0 -195
  132. package/dist/cli/commands/diagnose.js.map +0 -1
  133. package/dist/cli/commands/propose.d.ts +0 -18
  134. package/dist/cli/commands/propose.d.ts.map +0 -1
  135. package/dist/cli/commands/propose.js +0 -331
  136. package/dist/cli/commands/propose.js.map +0 -1
  137. package/dist/cli/commands/traces.d.ts +0 -15
  138. package/dist/cli/commands/traces.d.ts.map +0 -1
  139. package/dist/cli/commands/traces.js +0 -167
  140. package/dist/cli/commands/traces.js.map +0 -1
  141. package/dist/constraints/definitions.d.ts +0 -39
  142. package/dist/constraints/definitions.d.ts.map +0 -1
  143. package/dist/constraints/definitions.js +0 -71
  144. package/dist/constraints/definitions.js.map +0 -1
  145. package/dist/constraints/quality.d.ts +0 -11
  146. package/dist/constraints/quality.d.ts.map +0 -1
  147. package/dist/constraints/quality.js +0 -257
  148. package/dist/constraints/quality.js.map +0 -1
  149. package/dist/constraints/safety.d.ts +0 -13
  150. package/dist/constraints/safety.d.ts.map +0 -1
  151. package/dist/constraints/safety.js +0 -103
  152. package/dist/constraints/safety.js.map +0 -1
  153. package/dist/extensions/long-running/constraints.d.ts +0 -38
  154. package/dist/extensions/long-running/constraints.d.ts.map +0 -1
  155. package/dist/extensions/long-running/constraints.js +0 -153
  156. package/dist/extensions/long-running/constraints.js.map +0 -1
  157. package/dist/extensions/long-running/index.d.ts +0 -30
  158. package/dist/extensions/long-running/index.d.ts.map +0 -1
  159. package/dist/extensions/long-running/index.js +0 -50
  160. package/dist/extensions/long-running/index.js.map +0 -1
  161. package/dist/extensions/long-running/types.d.ts +0 -154
  162. package/dist/extensions/long-running/types.d.ts.map +0 -1
  163. package/dist/extensions/long-running/types.js +0 -9
  164. package/dist/extensions/long-running/types.js.map +0 -1
  165. package/dist/presets/long-running.d.ts +0 -33
  166. package/dist/presets/long-running.d.ts.map +0 -1
  167. package/dist/presets/long-running.js +0 -52
  168. package/dist/presets/long-running.js.map +0 -1
@@ -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
+ });
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
6
- import { KnowledgeStore } from '../store';
6
+ import { FileKnowledgeStore as KnowledgeStore } from '../store';
7
7
  import { KnowledgeIngest } from '../ingest';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { KnowledgeLifecycleHooks } from '../lifecycle-hooks';
6
- import { KnowledgeStore } from '../store';
6
+ import { FileKnowledgeStore as KnowledgeStore } from '../store';
7
7
  import { KnowledgeQuery } from '../query';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
6
- import { KnowledgeStore } from '../store';
6
+ import { FileKnowledgeStore as KnowledgeStore } from '../store';
7
7
  import { KnowledgeLifecycle } from '../lifecycle';
8
8
  import type { KnowledgeEntry } from '../types';
9
9
  import * as fs from 'fs';
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { KnowledgeLinter } from '../lint';
6
- import { KnowledgeStore } from '../store';
6
+ import { FileKnowledgeStore as KnowledgeStore } from '../store';
7
7
  import { ReferenceTracker } from '../reference-tracker';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
6
- import { KnowledgeStore } from '../store';
6
+ import { FileKnowledgeStore as KnowledgeStore } from '../store';
7
7
  import { KnowledgeQuery } from '../query';
8
8
  import type { KnowledgeEntry, QueryBudget } from '../types';
9
9
  import * as fs from 'fs';
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
6
- import { KnowledgeStore } from '../store';
6
+ import { FileKnowledgeStore as KnowledgeStore } from '../store';
7
7
  import { ReferenceTracker } from '../reference-tracker';
8
8
  import type { KnowledgeEntry } from '../types';
9
9
  import * as fs from 'fs';
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
6
- import { KnowledgeStore } from '../store';
6
+ import { FileKnowledgeStore as KnowledgeStore } from '../store';
7
7
  import type { KnowledgeEntry } from '../types';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
@@ -11,7 +11,8 @@
11
11
 
12
12
  import * as fs from 'fs';
13
13
  import * as path from 'path';
14
- import { KnowledgeStore } from './store';
14
+ import { FileKnowledgeStore } from './store';
15
+ import type { KnowledgeStore } from './store';
15
16
  import type { KnowledgeEntry, MaturityLevel } from './types';
16
17
 
17
18
  // ── Types ─────────────────────────────────────────────────
@@ -29,11 +30,14 @@ export type AuditRuleName =
29
30
  // D3: 去重有效性
30
31
  | 'title-duplicate'
31
32
  | 'source-refs-bloat'
33
+ | 'fragment-cluster'
32
34
  // D4: 成熟度健康
33
35
  | 'promotion-blocked'
34
36
  | 'orphan-draft'
35
37
  // D5: 新鲜度
36
- | 'stale-entry';
38
+ | 'stale-entry'
39
+ // D2: 领域相关性
40
+ | 'deprecated-domain';
37
41
 
38
42
  export type AuditAction = 'archive' | 'demote' | 'flag' | 'reject' | 'trim';
39
43
 
@@ -95,6 +99,12 @@ const EVENT_NOISE_PATTERNS = [
95
99
  /^\[Session Feature\]\s/,
96
100
  ];
97
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
+ }
98
108
 
99
109
  // ── Per-Entry Rules ───────────────────────────────────────
100
110
 
@@ -221,11 +231,12 @@ const perEntryRules: AuditRule[] = [
221
231
  detect: (entry, ctx) => {
222
232
  if (entry.maturity === 'archived') return null;
223
233
  if (!ctx.allEntries) return null;
234
+ if (!entry.title) return null;
224
235
  const dupes = ctx.allEntries.filter(e =>
225
236
  e.id !== entry.id &&
226
237
  e.maturity !== 'archived' &&
227
238
  e.type === entry.type &&
228
- e.title.toLowerCase().trim() === entry.title.toLowerCase().trim()
239
+ e.title?.toLowerCase().trim() === entry.title.toLowerCase().trim()
229
240
  );
230
241
  if (dupes.length > 0) {
231
242
  return `与 ${dupes.map(e => e.id).join(', ')} 标题重复`;
@@ -244,6 +255,31 @@ const perEntryRules: AuditRule[] = [
244
255
  return null;
245
256
  },
246
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
+ },
247
283
 
248
284
  // D4: 成熟度健康
249
285
  {
@@ -266,7 +302,7 @@ const perEntryRules: AuditRule[] = [
266
302
  action: 'flag',
267
303
  detect: (entry) => {
268
304
  if (entry.maturity !== 'draft') return null;
269
- 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) {
270
306
  return `draft 无贡献者/项目/引用`;
271
307
  }
272
308
  return null;
@@ -289,6 +325,27 @@ const perEntryRules: AuditRule[] = [
289
325
  return null;
290
326
  },
291
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
+ },
292
349
  ];
293
350
 
294
351
  // ── Audit Engine ──────────────────────────────────────────
@@ -300,7 +357,7 @@ export class KnowledgeAudit {
300
357
  private promotionBlockDays: number;
301
358
 
302
359
  constructor(options?: AuditOptions) {
303
- this.store = new KnowledgeStore({ baseDir: options?.baseDir });
360
+ this.store = new FileKnowledgeStore({ baseDir: options?.baseDir });
304
361
  this.shortContentThreshold = options?.shortContentThreshold ?? DEFAULT_SHORT_CONTENT_THRESHOLD;
305
362
  this.staleDays = options?.staleDays ?? DEFAULT_STALE_DAYS;
306
363
  this.promotionBlockDays = options?.promotionBlockDays ?? DEFAULT_PROMOTION_BLOCK_DAYS;
@@ -336,7 +393,7 @@ export class KnowledgeAudit {
336
393
  * 全量扫描(日兜底 / 手动模式)
337
394
  */
338
395
  run(options?: { autoFix?: boolean }): AuditReport {
339
- const entries = this.store.list({ excludeArchived: false });
396
+ const entries = this.store.list({ excludeArchived: false }).filter(e => e.title && e.type && e.content != null);
340
397
  const ctx: AuditContext = {
341
398
  shortContentThreshold: this.shortContentThreshold,
342
399
  staleDays: this.staleDays,
@@ -406,12 +463,12 @@ export class KnowledgeAudit {
406
463
  const d1Score = entries.length === 0 ? 100 : Math.max(0, 100 - (d1Issues.length / entries.length) * 100);
407
464
 
408
465
  // D2: 内容质量
409
- 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'];
410
467
  const d2Issues = issues.filter(i => d2Rules.includes(i.rule));
411
468
  const d2Score = active.length === 0 ? 100 : Math.max(0, 100 - (d2Issues.length / active.length) * 100);
412
469
 
413
470
  // D3: 去重有效性
414
- const d3Rules: AuditRuleName[] = ['title-duplicate', 'source-refs-bloat'];
471
+ const d3Rules: AuditRuleName[] = ['title-duplicate', 'source-refs-bloat', 'fragment-cluster'];
415
472
  const d3Issues = issues.filter(i => d3Rules.includes(i.rule));
416
473
  const d3Score = active.length === 0 ? 100 : Math.max(0, 100 - (d3Issues.length / active.length) * 100);
417
474
 
@@ -440,10 +497,10 @@ export class KnowledgeAudit {
440
497
  const d5Score = active.length === 0 ? 100 : Math.max(0, 100 - (staleRatio * 100));
441
498
 
442
499
  // D6: 飞轮验证
443
- const withRefs = active.filter(e => e.referencedBy.length > 0).length;
500
+ const withRefs = active.filter(e => genuineRefs(e.referencedBy).length > 0).length;
444
501
  const refCoverage = active.length > 0 ? withRefs / active.length : 0;
445
502
  const avgRefs = active.length > 0
446
- ? active.reduce((sum, e) => sum + e.referencedBy.length, 0) / active.length
503
+ ? active.reduce((sum, e) => sum + genuineRefs(e.referencedBy).length, 0) / active.length
447
504
  : 0;
448
505
 
449
506
  // Consumption hit rate from aggregated stats file (written by MonitorAgent)
@@ -5,7 +5,7 @@
5
5
  * MonitorAgent 周期性调用,低于 60 分触发 Triage 告警。
6
6
  */
7
7
 
8
- import { KnowledgeStore } from './store';
8
+ import type { KnowledgeStore } from './store';
9
9
  import { KnowledgeLinter } from './lint';
10
10
  import type { LintIssue } from './types';
11
11
 
@@ -14,7 +14,7 @@
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
16
  import type { KnowledgeEntry, KnowledgeType, MaturityLevel, StorageLayer } from './types';
17
- import { KnowledgeStore } from './store';
17
+ import type { KnowledgeStore } from './store';
18
18
 
19
19
  // ── 导入源 ───────────────────────────────────────────────
20
20