@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.
- package/CHANGELOG.md +11 -0
- package/bin/harness.js +71 -0
- 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 +140 -6
- 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 +80 -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/core/constraints/checker.js +1 -1
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/definitions.d.ts.map +1 -1
- package/dist/core/constraints/definitions.js +30 -24
- package/dist/core/constraints/definitions.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 +60 -8
- package/dist/knowledge/audit.js.map +1 -1
- package/dist/knowledge/doctor.d.ts +1 -1
- package/dist/knowledge/doctor.d.ts.map +1 -1
- package/dist/knowledge/import.d.ts +1 -1
- package/dist/knowledge/import.d.ts.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 +4 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +4 -2
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/ingest.d.ts +1 -1
- package/dist/knowledge/ingest.d.ts.map +1 -1
- package/dist/knowledge/ingest.js +2 -2
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle-hooks.d.ts +1 -1
- package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +16 -1
- 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.d.ts +1 -1
- package/dist/knowledge/lint.d.ts.map +1 -1
- package/dist/knowledge/lint.js +4 -4
- 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/query.d.ts +1 -1
- package/dist/knowledge/query.d.ts.map +1 -1
- package/dist/knowledge/reference-tracker.d.ts +1 -1
- package/dist/knowledge/reference-tracker.d.ts.map +1 -1
- package/dist/knowledge/store.d.ts +29 -2
- package/dist/knowledge/store.d.ts.map +1 -1
- package/dist/knowledge/store.js +10 -5
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +17 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/monitoring/knowledge-evolver.d.ts +1 -1
- package/dist/monitoring/knowledge-evolver.d.ts.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/dist/types/constraint.d.ts +1 -1
- package/dist/types/constraint.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/checker.test.ts +4 -4
- package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
- package/src/cli/commands/__tests__/knowledge.test.ts +70 -2
- package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -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 +151 -1
- package/src/cli/commands/release.ts +86 -12
- package/src/cli/commands/sdd.ts +44 -0
- package/src/cli/commands/spec-baseline-check.ts +384 -0
- package/src/context/__tests__/knowledge-injector.test.ts +1 -1
- package/src/core/constraints/checker.ts +1 -1
- package/src/core/constraints/definitions.ts +31 -25
- package/src/knowledge/__tests__/audit.test.ts +57 -1
- package/src/knowledge/__tests__/doctor.test.ts +1 -1
- package/src/knowledge/__tests__/import.test.ts +1 -1
- package/src/knowledge/__tests__/index-generator.test.ts +335 -0
- package/src/knowledge/__tests__/ingest.test.ts +1 -1
- package/src/knowledge/__tests__/lifecycle-hooks.test.ts +1 -1
- package/src/knowledge/__tests__/lifecycle.test.ts +1 -1
- package/src/knowledge/__tests__/lint.test.ts +1 -1
- package/src/knowledge/__tests__/query.test.ts +1 -1
- package/src/knowledge/__tests__/reference-tracker.test.ts +1 -1
- package/src/knowledge/__tests__/store.test.ts +1 -1
- package/src/knowledge/audit.ts +67 -10
- package/src/knowledge/doctor.ts +1 -1
- package/src/knowledge/import.ts +1 -1
- package/src/knowledge/index-generator.ts +233 -0
- package/src/knowledge/index.ts +4 -1
- package/src/knowledge/ingest.ts +3 -3
- package/src/knowledge/lifecycle-hooks.ts +1 -1
- package/src/knowledge/lifecycle.ts +72 -1
- package/src/knowledge/lint.ts +5 -5
- package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
- package/src/knowledge/primitives/code-structure.ts +161 -0
- package/src/knowledge/query.ts +1 -1
- package/src/knowledge/reference-tracker.ts +1 -1
- package/src/knowledge/store.ts +27 -4
- package/src/knowledge/types.ts +21 -1
- package/src/monitoring/knowledge-evolver.ts +1 -1
- package/src/sdd/__tests__/index-generator.test.ts +150 -0
- package/src/sdd/index-generator.ts +109 -0
- package/src/types/constraint.ts +1 -1
- package/dist/cli/commands/diagnose.d.ts +0 -16
- package/dist/cli/commands/diagnose.d.ts.map +0 -1
- package/dist/cli/commands/diagnose.js +0 -195
- package/dist/cli/commands/diagnose.js.map +0 -1
- package/dist/cli/commands/propose.d.ts +0 -18
- package/dist/cli/commands/propose.d.ts.map +0 -1
- package/dist/cli/commands/propose.js +0 -331
- package/dist/cli/commands/propose.js.map +0 -1
- package/dist/cli/commands/traces.d.ts +0 -15
- package/dist/cli/commands/traces.d.ts.map +0 -1
- package/dist/cli/commands/traces.js +0 -167
- package/dist/cli/commands/traces.js.map +0 -1
- package/dist/constraints/definitions.d.ts +0 -39
- package/dist/constraints/definitions.d.ts.map +0 -1
- package/dist/constraints/definitions.js +0 -71
- package/dist/constraints/definitions.js.map +0 -1
- package/dist/constraints/quality.d.ts +0 -11
- package/dist/constraints/quality.d.ts.map +0 -1
- package/dist/constraints/quality.js +0 -257
- package/dist/constraints/quality.js.map +0 -1
- package/dist/constraints/safety.d.ts +0 -13
- package/dist/constraints/safety.d.ts.map +0 -1
- package/dist/constraints/safety.js +0 -103
- package/dist/constraints/safety.js.map +0 -1
- package/dist/extensions/long-running/constraints.d.ts +0 -38
- package/dist/extensions/long-running/constraints.d.ts.map +0 -1
- package/dist/extensions/long-running/constraints.js +0 -153
- package/dist/extensions/long-running/constraints.js.map +0 -1
- package/dist/extensions/long-running/index.d.ts +0 -30
- package/dist/extensions/long-running/index.d.ts.map +0 -1
- package/dist/extensions/long-running/index.js +0 -50
- package/dist/extensions/long-running/index.js.map +0 -1
- package/dist/extensions/long-running/types.d.ts +0 -154
- package/dist/extensions/long-running/types.d.ts.map +0 -1
- package/dist/extensions/long-running/types.js +0 -9
- package/dist/extensions/long-running/types.js.map +0 -1
- package/dist/presets/long-running.d.ts +0 -33
- package/dist/presets/long-running.d.ts.map +0 -1
- package/dist/presets/long-running.js +0 -52
- 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';
|
package/src/knowledge/audit.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
|
-
import {
|
|
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
|
|
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
|
|
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)
|
package/src/knowledge/doctor.ts
CHANGED
package/src/knowledge/import.ts
CHANGED
|
@@ -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
|
|