@dommaker/harness 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/harness.js +73 -1
- package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
- package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
- package/dist/cli/commands/doc-freshness-check.js +366 -0
- package/dist/cli/commands/doc-freshness-check.js.map +1 -0
- package/dist/cli/commands/index.d.ts +4 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +13 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/knowledge.d.ts +12 -0
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +134 -0
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/release.d.ts +11 -0
- package/dist/cli/commands/release.d.ts.map +1 -1
- package/dist/cli/commands/release.js +43 -12
- package/dist/cli/commands/release.js.map +1 -1
- package/dist/cli/commands/sdd.d.ts +8 -0
- package/dist/cli/commands/sdd.d.ts.map +1 -0
- package/dist/cli/commands/sdd.js +68 -0
- package/dist/cli/commands/sdd.js.map +1 -0
- package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
- package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
- package/dist/cli/commands/spec-baseline-check.js +366 -0
- package/dist/cli/commands/spec-baseline-check.js.map +1 -0
- package/dist/cli/commands/sync-docs.d.ts +2 -0
- package/dist/cli/commands/sync-docs.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs.js +320 -1
- package/dist/cli/commands/sync-docs.js.map +1 -1
- package/dist/knowledge/audit.d.ts +1 -1
- package/dist/knowledge/audit.d.ts.map +1 -1
- package/dist/knowledge/audit.js +59 -7
- package/dist/knowledge/audit.js.map +1 -1
- package/dist/knowledge/index-generator.d.ts +36 -0
- package/dist/knowledge/index-generator.d.ts.map +1 -0
- package/dist/knowledge/index-generator.js +241 -0
- package/dist/knowledge/index-generator.js.map +1 -0
- package/dist/knowledge/index.d.ts +2 -0
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +3 -1
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/ingest.js +2 -2
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +15 -0
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +70 -0
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/knowledge/lint.js +3 -3
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/primitives/code-structure.d.ts +37 -0
- package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
- package/dist/knowledge/primitives/code-structure.js +168 -0
- package/dist/knowledge/primitives/code-structure.js.map +1 -0
- package/dist/knowledge/types.d.ts +3 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/sdd/index-generator.d.ts +22 -0
- package/dist/sdd/index-generator.d.ts.map +1 -0
- package/dist/sdd/index-generator.js +118 -0
- package/dist/sdd/index-generator.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/CONTEXT.md +3 -3
- package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
- package/src/cli/commands/__tests__/knowledge.test.ts +69 -1
- package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +226 -0
- package/src/cli/commands/doc-freshness-check.ts +397 -0
- package/src/cli/commands/index.ts +4 -1
- package/src/cli/commands/knowledge.ts +150 -0
- package/src/cli/commands/release.ts +48 -12
- package/src/cli/commands/sdd.ts +44 -0
- package/src/cli/commands/spec-baseline-check.ts +384 -0
- package/src/cli/commands/sync-docs.ts +355 -1
- package/src/knowledge/__tests__/audit.test.ts +56 -0
- package/src/knowledge/__tests__/index-generator.test.ts +335 -0
- package/src/knowledge/audit.ts +64 -8
- package/src/knowledge/index-generator.ts +233 -0
- package/src/knowledge/index.ts +2 -0
- package/src/knowledge/ingest.ts +2 -2
- package/src/knowledge/lifecycle.ts +71 -0
- package/src/knowledge/lint.ts +3 -3
- package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
- package/src/knowledge/primitives/code-structure.ts +161 -0
- package/src/knowledge/types.ts +4 -1
- package/src/sdd/__tests__/index-generator.test.ts +150 -0
- 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
|
+
});
|
package/src/knowledge/audit.ts
CHANGED
|
@@ -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
|
|
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)
|