@dommaker/harness 0.13.3 → 0.14.0
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 +24 -0
- package/bin/harness-knowledge-check.js +38 -23
- package/bin/harness-knowledge-track.js +30 -4
- package/bin/harness.js +24 -1
- package/dist/cli/commands/diagnose.d.ts +16 -0
- package/dist/cli/commands/diagnose.d.ts.map +1 -0
- package/dist/cli/commands/diagnose.js +195 -0
- package/dist/cli/commands/diagnose.js.map +1 -0
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +4 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/knowledge.d.ts +21 -0
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +155 -4
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/propose.d.ts +18 -0
- package/dist/cli/commands/propose.d.ts.map +1 -0
- package/dist/cli/commands/propose.js +331 -0
- package/dist/cli/commands/propose.js.map +1 -0
- package/dist/cli/commands/traces.d.ts +15 -0
- package/dist/cli/commands/traces.d.ts.map +1 -0
- package/dist/cli/commands/traces.js +167 -0
- package/dist/cli/commands/traces.js.map +1 -0
- package/dist/constraints/definitions.d.ts +39 -0
- package/dist/constraints/definitions.d.ts.map +1 -0
- package/dist/constraints/definitions.js +71 -0
- package/dist/constraints/definitions.js.map +1 -0
- package/dist/constraints/quality.d.ts +11 -0
- package/dist/constraints/quality.d.ts.map +1 -0
- package/dist/constraints/quality.js +257 -0
- package/dist/constraints/quality.js.map +1 -0
- package/dist/constraints/safety.d.ts +13 -0
- package/dist/constraints/safety.d.ts.map +1 -0
- package/dist/constraints/safety.js +103 -0
- package/dist/constraints/safety.js.map +1 -0
- package/dist/core/constraints/definitions.d.ts.map +1 -1
- package/dist/core/constraints/definitions.js +4 -2
- package/dist/core/constraints/definitions.js.map +1 -1
- package/dist/extensions/long-running/constraints.d.ts +38 -0
- package/dist/extensions/long-running/constraints.d.ts.map +1 -0
- package/dist/extensions/long-running/constraints.js +153 -0
- package/dist/extensions/long-running/constraints.js.map +1 -0
- package/dist/extensions/long-running/index.d.ts +30 -0
- package/dist/extensions/long-running/index.d.ts.map +1 -0
- package/dist/extensions/long-running/index.js +50 -0
- package/dist/extensions/long-running/index.js.map +1 -0
- package/dist/extensions/long-running/types.d.ts +154 -0
- package/dist/extensions/long-running/types.d.ts.map +1 -0
- package/dist/extensions/long-running/types.js +9 -0
- package/dist/extensions/long-running/types.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/knowledge/audit.d.ts +76 -0
- package/dist/knowledge/audit.d.ts.map +1 -0
- package/dist/knowledge/audit.js +566 -0
- package/dist/knowledge/audit.js.map +1 -0
- package/dist/knowledge/import.d.ts.map +1 -1
- package/dist/knowledge/import.js +3 -0
- package/dist/knowledge/import.js.map +1 -1
- package/dist/knowledge/index.d.ts +5 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +6 -1
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/ingest.d.ts +27 -0
- package/dist/knowledge/ingest.d.ts.map +1 -1
- package/dist/knowledge/ingest.js +150 -5
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
- package/dist/knowledge/lifecycle-hooks.js +3 -0
- package/dist/knowledge/lifecycle-hooks.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +59 -7
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +213 -52
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/knowledge/lint.d.ts.map +1 -1
- package/dist/knowledge/lint.js +43 -2
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/migration.d.ts +25 -0
- package/dist/knowledge/migration.d.ts.map +1 -0
- package/dist/knowledge/migration.js +96 -0
- package/dist/knowledge/migration.js.map +1 -0
- package/dist/knowledge/query.d.ts +29 -1
- package/dist/knowledge/query.d.ts.map +1 -1
- package/dist/knowledge/query.js +58 -0
- package/dist/knowledge/query.js.map +1 -1
- package/dist/knowledge/store.d.ts +21 -1
- package/dist/knowledge/store.d.ts.map +1 -1
- package/dist/knowledge/store.js +103 -6
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +29 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/types.js.map +1 -1
- package/dist/presets/long-running.d.ts +33 -0
- package/dist/presets/long-running.d.ts.map +1 -0
- package/dist/presets/long-running.js +52 -0
- package/dist/presets/long-running.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/__tests__/knowledge.test.ts +167 -0
- package/src/cli/commands/index.ts +1 -1
- package/src/cli/commands/knowledge.ts +175 -4
- package/src/context/__tests__/knowledge-injector.test.ts +3 -0
- package/src/core/constraints/definitions.ts +4 -2
- package/src/dashboard/__tests__/data.test.ts +3 -0
- package/src/dashboard/__tests__/stats.test.ts +3 -0
- package/src/index.ts +3 -1
- package/src/knowledge/CONTEXT.md +29 -7
- package/src/knowledge/__tests__/audit.test.ts +598 -0
- package/src/knowledge/__tests__/doctor.test.ts +27 -0
- package/src/knowledge/__tests__/ingest.test.ts +319 -0
- package/src/knowledge/__tests__/lifecycle-hooks.test.ts +3 -0
- package/src/knowledge/__tests__/lifecycle.test.ts +655 -2
- package/src/knowledge/__tests__/lint.test.ts +51 -0
- package/src/knowledge/__tests__/migration.test.ts +115 -0
- package/src/knowledge/__tests__/query.test.ts +86 -0
- package/src/knowledge/__tests__/reference-tracker.test.ts +3 -0
- package/src/knowledge/__tests__/store.test.ts +207 -0
- package/src/knowledge/audit.ts +640 -0
- package/src/knowledge/import.ts +3 -0
- package/src/knowledge/index.ts +5 -1
- package/src/knowledge/ingest.ts +169 -5
- package/src/knowledge/lifecycle-hooks.ts +3 -0
- package/src/knowledge/lifecycle.ts +218 -49
- package/src/knowledge/lint.ts +43 -2
- package/src/knowledge/migration.ts +73 -0
- package/src/knowledge/query.ts +71 -0
- package/src/knowledge/store.ts +100 -6
- package/src/knowledge/types.ts +33 -1
- package/src/monitoring/__tests__/knowledge-doctor.test.ts +3 -0
- package/src/monitoring/__tests__/knowledge-evolver.test.ts +3 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KnowledgeAudit tests
|
|
3
|
+
*
|
|
4
|
+
* Covers all 11 rules across 6 dimensions:
|
|
5
|
+
* D1: frontmatter-missing
|
|
6
|
+
* D2: test-data-pollution, daily-audit-noise, event-noise, zero-content-proven, short-content, maturity-inflation
|
|
7
|
+
* D3: title-duplicate, source-refs-bloat
|
|
8
|
+
* D4: promotion-blocked, orphan-draft
|
|
9
|
+
* D5: stale-entry
|
|
10
|
+
* D6: flywheel (computed from referencedBy)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import { KnowledgeAudit } from '../audit';
|
|
17
|
+
import { KnowledgeStore } from '../store';
|
|
18
|
+
import type { KnowledgeEntry } from '../types';
|
|
19
|
+
|
|
20
|
+
function makeTmpDir(): string {
|
|
21
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'audit-test-'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeEntry(overrides: Partial<KnowledgeEntry> = {}): KnowledgeEntry {
|
|
25
|
+
return {
|
|
26
|
+
id: 'TEST-001',
|
|
27
|
+
type: 'guideline',
|
|
28
|
+
title: 'Test Entry',
|
|
29
|
+
content: 'Some meaningful content that is long enough to pass thresholds',
|
|
30
|
+
maturity: 'draft',
|
|
31
|
+
layer: 'project',
|
|
32
|
+
created: new Date().toISOString(),
|
|
33
|
+
lastReferenced: new Date().toISOString(),
|
|
34
|
+
contributors: ['test-user'],
|
|
35
|
+
projects: ['test-project'],
|
|
36
|
+
tags: [],
|
|
37
|
+
applicablePhases: [],
|
|
38
|
+
sourceReferences: [],
|
|
39
|
+
referencedBy: [],
|
|
40
|
+
executionResults: [],
|
|
41
|
+
consumptionMode: 'reference',
|
|
42
|
+
origin: 'agent',
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setupStore(dir: string, entries: Partial<KnowledgeEntry>[]): KnowledgeStore {
|
|
48
|
+
const store = new KnowledgeStore({ baseDir: dir });
|
|
49
|
+
for (const partial of entries) {
|
|
50
|
+
const entry = makeEntry(partial);
|
|
51
|
+
store.save(entry);
|
|
52
|
+
}
|
|
53
|
+
return store;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── D1: frontmatter-missing ─────────────────────────────
|
|
57
|
+
|
|
58
|
+
describe('D1: frontmatter-missing', () => {
|
|
59
|
+
it('rejects entry with missing id', () => {
|
|
60
|
+
const dir = makeTmpDir();
|
|
61
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
62
|
+
const entry = makeEntry({ id: '' as any });
|
|
63
|
+
const issues = audit.validate(entry);
|
|
64
|
+
const fm = issues.filter(i => i.rule === 'frontmatter-missing');
|
|
65
|
+
expect(fm.length).toBe(1);
|
|
66
|
+
expect(fm[0].action).toBe('reject');
|
|
67
|
+
fs.rmSync(dir, { recursive: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('rejects entry with missing type', () => {
|
|
71
|
+
const dir = makeTmpDir();
|
|
72
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
73
|
+
const entry = makeEntry({ type: undefined as any });
|
|
74
|
+
const issues = audit.validate(entry);
|
|
75
|
+
expect(issues.some(i => i.rule === 'frontmatter-missing')).toBe(true);
|
|
76
|
+
fs.rmSync(dir, { recursive: true });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('passes with all required fields present', () => {
|
|
80
|
+
const dir = makeTmpDir();
|
|
81
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
82
|
+
const entry = makeEntry();
|
|
83
|
+
const issues = audit.validate(entry);
|
|
84
|
+
expect(issues.filter(i => i.rule === 'frontmatter-missing')).toHaveLength(0);
|
|
85
|
+
fs.rmSync(dir, { recursive: true });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── D2: test-data-pollution ─────────────────────────────
|
|
90
|
+
|
|
91
|
+
describe('D2: test-data-pollution', () => {
|
|
92
|
+
it('archives entries with test-scope tags', () => {
|
|
93
|
+
const dir = makeTmpDir();
|
|
94
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
95
|
+
const entry = makeEntry({ tags: ['test-scope-abc'] });
|
|
96
|
+
const issues = audit.validate(entry);
|
|
97
|
+
expect(issues.some(i => i.rule === 'test-data-pollution' && i.action === 'archive')).toBe(true);
|
|
98
|
+
fs.rmSync(dir, { recursive: true });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('archives entries with test title', () => {
|
|
102
|
+
const dir = makeTmpDir();
|
|
103
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
104
|
+
const entry = makeEntry({ title: 'Test Entry' });
|
|
105
|
+
const issues = audit.validate(entry);
|
|
106
|
+
expect(issues.some(i => i.rule === 'test-data-pollution')).toBe(true);
|
|
107
|
+
fs.rmSync(dir, { recursive: true });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('archives entries with test-* ID', () => {
|
|
111
|
+
const dir = makeTmpDir();
|
|
112
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
113
|
+
const entry = makeEntry({ id: 'test-search-1780414709970-pattern' });
|
|
114
|
+
const issues = audit.validate(entry);
|
|
115
|
+
expect(issues.some(i => i.rule === 'test-data-pollution' && i.detail.includes('test-search'))).toBe(true);
|
|
116
|
+
fs.rmSync(dir, { recursive: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('archives entries with inj-test-* ID', () => {
|
|
120
|
+
const dir = makeTmpDir();
|
|
121
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
122
|
+
const entry = makeEntry({ id: 'inj-test-1780414711344' });
|
|
123
|
+
const issues = audit.validate(entry);
|
|
124
|
+
expect(issues.some(i => i.rule === 'test-data-pollution' && i.detail.includes('inj-test'))).toBe(true);
|
|
125
|
+
fs.rmSync(dir, { recursive: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('skips archived entries', () => {
|
|
129
|
+
const dir = makeTmpDir();
|
|
130
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
131
|
+
const entry = makeEntry({ maturity: 'archived', tags: ['test-scope-abc'] });
|
|
132
|
+
const issues = audit.validate(entry);
|
|
133
|
+
expect(issues.filter(i => i.rule === 'test-data-pollution')).toHaveLength(0);
|
|
134
|
+
fs.rmSync(dir, { recursive: true });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── D2: daily-audit-noise ──────────────────────────────
|
|
139
|
+
|
|
140
|
+
describe('D2: daily-audit-noise', () => {
|
|
141
|
+
it('archives entries with [Auditor] Daily audit title', () => {
|
|
142
|
+
const dir = makeTmpDir();
|
|
143
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
144
|
+
const entry = makeEntry({ title: '[Auditor] Daily audit 2026-06-01' });
|
|
145
|
+
const issues = audit.validate(entry);
|
|
146
|
+
expect(issues.some(i => i.rule === 'daily-audit-noise' && i.action === 'archive')).toBe(true);
|
|
147
|
+
fs.rmSync(dir, { recursive: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('passes for normal titles', () => {
|
|
151
|
+
const dir = makeTmpDir();
|
|
152
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
153
|
+
const entry = makeEntry({ title: 'Normal knowledge entry' });
|
|
154
|
+
const issues = audit.validate(entry);
|
|
155
|
+
expect(issues.filter(i => i.rule === 'daily-audit-noise')).toHaveLength(0);
|
|
156
|
+
fs.rmSync(dir, { recursive: true });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── D2: event-noise ────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe('D2: event-noise', () => {
|
|
163
|
+
it('rejects [Monitor] event titles', () => {
|
|
164
|
+
const dir = makeTmpDir();
|
|
165
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
166
|
+
const entry = makeEntry({ title: '[Monitor] stuck_goals: GoalExecution abc123' });
|
|
167
|
+
const issues = audit.validate(entry);
|
|
168
|
+
expect(issues.some(i => i.rule === 'event-noise' && i.action === 'archive')).toBe(true);
|
|
169
|
+
fs.rmSync(dir, { recursive: true });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('rejects KnowledgeSync cycle titles', () => {
|
|
173
|
+
const dir = makeTmpDir();
|
|
174
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
175
|
+
const entry = makeEntry({ title: 'KnowledgeSync cycle: 0 stale, 0 unmonitored' });
|
|
176
|
+
const issues = audit.validate(entry);
|
|
177
|
+
expect(issues.some(i => i.rule === 'event-noise' && i.action === 'archive')).toBe(true);
|
|
178
|
+
fs.rmSync(dir, { recursive: true });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('rejects [Triage Fix] titles', () => {
|
|
182
|
+
const dir = makeTmpDir();
|
|
183
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
184
|
+
const entry = makeEntry({ title: '[Triage Fix] timeout (critical)' });
|
|
185
|
+
const issues = audit.validate(entry);
|
|
186
|
+
expect(issues.some(i => i.rule === 'event-noise' && i.action === 'archive')).toBe(true);
|
|
187
|
+
fs.rmSync(dir, { recursive: true });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('rejects [Session Feature] titles', () => {
|
|
191
|
+
const dir = makeTmpDir();
|
|
192
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
193
|
+
const entry = makeEntry({ title: '[Session Feature] feat: studio release' });
|
|
194
|
+
const issues = audit.validate(entry);
|
|
195
|
+
expect(issues.some(i => i.rule === 'event-noise' && i.action === 'archive')).toBe(true);
|
|
196
|
+
fs.rmSync(dir, { recursive: true });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('passes for normal knowledge titles', () => {
|
|
200
|
+
const dir = makeTmpDir();
|
|
201
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
202
|
+
const entry = makeEntry({ title: 'SQLite WAL mode pattern' });
|
|
203
|
+
const issues = audit.validate(entry);
|
|
204
|
+
expect(issues.filter(i => i.rule === 'event-noise')).toHaveLength(0);
|
|
205
|
+
fs.rmSync(dir, { recursive: true });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('skips archived entries', () => {
|
|
209
|
+
const dir = makeTmpDir();
|
|
210
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
211
|
+
const entry = makeEntry({ maturity: 'archived', title: '[Monitor] stuck_goals: test' });
|
|
212
|
+
const issues = audit.validate(entry);
|
|
213
|
+
expect(issues.filter(i => i.rule === 'event-noise')).toHaveLength(0);
|
|
214
|
+
fs.rmSync(dir, { recursive: true });
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── D2: zero-content-proven ────────────────────────────
|
|
219
|
+
|
|
220
|
+
describe('D2: zero-content-proven', () => {
|
|
221
|
+
it('demotes proven entries with <20 chars content', () => {
|
|
222
|
+
const dir = makeTmpDir();
|
|
223
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
224
|
+
const entry = makeEntry({ maturity: 'proven', content: 'short' });
|
|
225
|
+
const issues = audit.validate(entry);
|
|
226
|
+
expect(issues.some(i => i.rule === 'zero-content-proven' && i.action === 'demote')).toBe(true);
|
|
227
|
+
fs.rmSync(dir, { recursive: true });
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('passes for proven entries with sufficient content', () => {
|
|
231
|
+
const dir = makeTmpDir();
|
|
232
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
233
|
+
const entry = makeEntry({ maturity: 'proven', content: 'a'.repeat(50) });
|
|
234
|
+
const issues = audit.validate(entry);
|
|
235
|
+
expect(issues.filter(i => i.rule === 'zero-content-proven')).toHaveLength(0);
|
|
236
|
+
fs.rmSync(dir, { recursive: true });
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── D2: maturity-inflation ─────────────────────────────
|
|
241
|
+
|
|
242
|
+
describe('D2: maturity-inflation', () => {
|
|
243
|
+
it('demotes verified entries with <20 chars content', () => {
|
|
244
|
+
const dir = makeTmpDir();
|
|
245
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
246
|
+
const entry = makeEntry({ maturity: 'verified', content: 'tiny' });
|
|
247
|
+
const issues = audit.validate(entry);
|
|
248
|
+
expect(issues.some(i => i.rule === 'maturity-inflation' && i.action === 'demote')).toBe(true);
|
|
249
|
+
fs.rmSync(dir, { recursive: true });
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ── D2: short-content ──────────────────────────────────
|
|
254
|
+
|
|
255
|
+
describe('D2: short-content', () => {
|
|
256
|
+
it('flags entries with content between 20-50 chars', () => {
|
|
257
|
+
const dir = makeTmpDir();
|
|
258
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
259
|
+
// 25 chars — above zero threshold (20) but below short threshold (50)
|
|
260
|
+
const entry = makeEntry({ content: 'a'.repeat(25), maturity: 'draft' });
|
|
261
|
+
const issues = audit.validate(entry);
|
|
262
|
+
expect(issues.some(i => i.rule === 'short-content' && i.action === 'flag')).toBe(true);
|
|
263
|
+
fs.rmSync(dir, { recursive: true });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('respects custom threshold', () => {
|
|
267
|
+
const dir = makeTmpDir();
|
|
268
|
+
const audit = new KnowledgeAudit({ baseDir: dir, shortContentThreshold: 100 });
|
|
269
|
+
const entry = makeEntry({ content: 'a'.repeat(60), maturity: 'draft' });
|
|
270
|
+
const issues = audit.validate(entry);
|
|
271
|
+
expect(issues.some(i => i.rule === 'short-content')).toBe(true);
|
|
272
|
+
fs.rmSync(dir, { recursive: true });
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── D3: title-duplicate ────────────────────────────────
|
|
277
|
+
|
|
278
|
+
describe('D3: title-duplicate', () => {
|
|
279
|
+
it('flags entries with duplicate titles of same type', () => {
|
|
280
|
+
const dir = makeTmpDir();
|
|
281
|
+
setupStore(dir, [
|
|
282
|
+
{ id: 'A-001', title: 'Same Title', type: 'guideline' },
|
|
283
|
+
{ id: 'A-002', title: 'Same Title', type: 'guideline' },
|
|
284
|
+
]);
|
|
285
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
286
|
+
const report = audit.run();
|
|
287
|
+
const dupes = report.issues.filter(i => i.rule === 'title-duplicate');
|
|
288
|
+
expect(dupes.length).toBe(2);
|
|
289
|
+
fs.rmSync(dir, { recursive: true });
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('ignores archived entries in dedup', () => {
|
|
293
|
+
const dir = makeTmpDir();
|
|
294
|
+
setupStore(dir, [
|
|
295
|
+
{ id: 'A-001', title: 'Same Title', type: 'guideline', maturity: 'archived' },
|
|
296
|
+
{ id: 'A-002', title: 'Same Title', type: 'guideline' },
|
|
297
|
+
]);
|
|
298
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
299
|
+
const report = audit.run();
|
|
300
|
+
const dupes = report.issues.filter(i => i.rule === 'title-duplicate');
|
|
301
|
+
expect(dupes.length).toBe(0);
|
|
302
|
+
fs.rmSync(dir, { recursive: true });
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ── D3: source-refs-bloat ──────────────────────────────
|
|
307
|
+
|
|
308
|
+
describe('D3: source-refs-bloat', () => {
|
|
309
|
+
it('flags entries with >20 source references', () => {
|
|
310
|
+
const dir = makeTmpDir();
|
|
311
|
+
const refs = Array.from({ length: 25 }, (_, i) => ({
|
|
312
|
+
workflow: `wf-${i}`,
|
|
313
|
+
timestamp: new Date().toISOString(),
|
|
314
|
+
}));
|
|
315
|
+
setupStore(dir, [{ id: 'B-001', sourceReferences: refs }]);
|
|
316
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
317
|
+
const report = audit.run();
|
|
318
|
+
expect(report.issues.some(i => i.rule === 'source-refs-bloat')).toBe(true);
|
|
319
|
+
fs.rmSync(dir, { recursive: true });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('trims source references on auto-fix', () => {
|
|
323
|
+
const dir = makeTmpDir();
|
|
324
|
+
const refs = Array.from({ length: 25 }, (_, i) => ({
|
|
325
|
+
workflow: `wf-${i}`,
|
|
326
|
+
timestamp: new Date().toISOString(),
|
|
327
|
+
}));
|
|
328
|
+
setupStore(dir, [{ id: 'B-001', sourceReferences: refs, title: 'Valid Knowledge Title' }]);
|
|
329
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
330
|
+
const report = audit.run({ autoFix: true });
|
|
331
|
+
expect(report.autoFixed).toBeGreaterThan(0);
|
|
332
|
+
// re-read from disk after fix
|
|
333
|
+
const store2 = new KnowledgeStore({ baseDir: dir });
|
|
334
|
+
const updated = store2.get('B-001');
|
|
335
|
+
expect(updated!.sourceReferences.length).toBe(20);
|
|
336
|
+
fs.rmSync(dir, { recursive: true });
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ── D4: promotion-blocked ──────────────────────────────
|
|
341
|
+
|
|
342
|
+
describe('D4: promotion-blocked', () => {
|
|
343
|
+
it('flags draft entries older than 30 days with no references', () => {
|
|
344
|
+
const dir = makeTmpDir();
|
|
345
|
+
const oldDate = new Date(Date.now() - 60 * 24 * 3600_000).toISOString();
|
|
346
|
+
setupStore(dir, [{
|
|
347
|
+
id: 'C-001',
|
|
348
|
+
maturity: 'draft',
|
|
349
|
+
created: oldDate,
|
|
350
|
+
lastReferenced: '',
|
|
351
|
+
}]);
|
|
352
|
+
const audit = new KnowledgeAudit({ baseDir: dir, promotionBlockDays: 30 });
|
|
353
|
+
const report = audit.run();
|
|
354
|
+
expect(report.issues.some(i => i.rule === 'promotion-blocked')).toBe(true);
|
|
355
|
+
fs.rmSync(dir, { recursive: true });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('ignores non-draft entries', () => {
|
|
359
|
+
const dir = makeTmpDir();
|
|
360
|
+
const oldDate = new Date(Date.now() - 60 * 24 * 3600_000).toISOString();
|
|
361
|
+
setupStore(dir, [{
|
|
362
|
+
id: 'C-002',
|
|
363
|
+
maturity: 'verified',
|
|
364
|
+
created: oldDate,
|
|
365
|
+
lastReferenced: '',
|
|
366
|
+
}]);
|
|
367
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
368
|
+
const report = audit.run();
|
|
369
|
+
expect(report.issues.filter(i => i.rule === 'promotion-blocked')).toHaveLength(0);
|
|
370
|
+
fs.rmSync(dir, { recursive: true });
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ── D4: orphan-draft ───────────────────────────────────
|
|
375
|
+
|
|
376
|
+
describe('D4: orphan-draft', () => {
|
|
377
|
+
it('flags drafts with no contributors/projects/references', () => {
|
|
378
|
+
const dir = makeTmpDir();
|
|
379
|
+
setupStore(dir, [{
|
|
380
|
+
id: 'D-001',
|
|
381
|
+
maturity: 'draft',
|
|
382
|
+
contributors: [],
|
|
383
|
+
projects: [],
|
|
384
|
+
referencedBy: [],
|
|
385
|
+
}]);
|
|
386
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
387
|
+
const report = audit.run();
|
|
388
|
+
expect(report.issues.some(i => i.rule === 'orphan-draft')).toBe(true);
|
|
389
|
+
fs.rmSync(dir, { recursive: true });
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('passes for drafts with contributors', () => {
|
|
393
|
+
const dir = makeTmpDir();
|
|
394
|
+
setupStore(dir, [{
|
|
395
|
+
id: 'D-002',
|
|
396
|
+
maturity: 'draft',
|
|
397
|
+
contributors: ['user-a'],
|
|
398
|
+
projects: [],
|
|
399
|
+
referencedBy: [],
|
|
400
|
+
}]);
|
|
401
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
402
|
+
const report = audit.run();
|
|
403
|
+
expect(report.issues.filter(i => i.rule === 'orphan-draft')).toHaveLength(0);
|
|
404
|
+
fs.rmSync(dir, { recursive: true });
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ── D5: stale-entry ────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
describe('D5: stale-entry', () => {
|
|
411
|
+
it('flags entries older than staleDays threshold', () => {
|
|
412
|
+
const dir = makeTmpDir();
|
|
413
|
+
const oldDate = new Date(Date.now() - 120 * 24 * 3600_000).toISOString();
|
|
414
|
+
setupStore(dir, [{
|
|
415
|
+
id: 'E-001',
|
|
416
|
+
maturity: 'verified',
|
|
417
|
+
lastReferenced: oldDate,
|
|
418
|
+
}]);
|
|
419
|
+
const audit = new KnowledgeAudit({ baseDir: dir, staleDays: 90 });
|
|
420
|
+
const report = audit.run();
|
|
421
|
+
expect(report.issues.some(i => i.rule === 'stale-entry')).toBe(true);
|
|
422
|
+
fs.rmSync(dir, { recursive: true });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('ignores archived entries', () => {
|
|
426
|
+
const dir = makeTmpDir();
|
|
427
|
+
const oldDate = new Date(Date.now() - 120 * 24 * 3600_000).toISOString();
|
|
428
|
+
setupStore(dir, [{
|
|
429
|
+
id: 'E-002',
|
|
430
|
+
maturity: 'archived',
|
|
431
|
+
lastReferenced: oldDate,
|
|
432
|
+
}]);
|
|
433
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
434
|
+
const report = audit.run();
|
|
435
|
+
expect(report.issues.filter(i => i.rule === 'stale-entry')).toHaveLength(0);
|
|
436
|
+
fs.rmSync(dir, { recursive: true });
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ── D6: flywheel dimension ─────────────────────────────
|
|
441
|
+
|
|
442
|
+
describe('D6: flywheel dimension', () => {
|
|
443
|
+
it('computes refCoverage from referencedBy', () => {
|
|
444
|
+
const dir = makeTmpDir();
|
|
445
|
+
setupStore(dir, [
|
|
446
|
+
{ id: 'F-001', maturity: 'verified', referencedBy: ['agent-1', 'agent-2'] },
|
|
447
|
+
{ id: 'F-002', maturity: 'verified', referencedBy: [] },
|
|
448
|
+
{ id: 'F-003', maturity: 'verified', referencedBy: ['agent-1'] },
|
|
449
|
+
]);
|
|
450
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
451
|
+
const report = audit.run();
|
|
452
|
+
// 2/3 entries have refs → 67% coverage
|
|
453
|
+
expect(report.dimensions.flywheel.details.refCoverage).toBe(67);
|
|
454
|
+
expect(report.dimensions.flywheel.score).toBeGreaterThan(0);
|
|
455
|
+
fs.rmSync(dir, { recursive: true });
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('gives 0 score for empty store (no entries to measure)', () => {
|
|
459
|
+
const dir = makeTmpDir();
|
|
460
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
461
|
+
const report = audit.run();
|
|
462
|
+
// 0/0 = no data → score 0 (no flywheel activity)
|
|
463
|
+
expect(report.dimensions.flywheel.score).toBe(0);
|
|
464
|
+
expect(report.totalEntries).toBe(0);
|
|
465
|
+
fs.rmSync(dir, { recursive: true });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('boosts D6 score when consumption stats file exists', () => {
|
|
469
|
+
const dir = makeTmpDir();
|
|
470
|
+
setupStore(dir, [
|
|
471
|
+
{ id: 'F-001', maturity: 'verified', referencedBy: [] },
|
|
472
|
+
{ id: 'F-002', maturity: 'verified', referencedBy: [] },
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
// Without consumption stats → low D6 (no refs)
|
|
476
|
+
const audit1 = new KnowledgeAudit({ baseDir: dir });
|
|
477
|
+
const report1 = audit1.run();
|
|
478
|
+
const baseD6 = report1.dimensions.flywheel.score;
|
|
479
|
+
|
|
480
|
+
// Write consumption stats file
|
|
481
|
+
fs.writeFileSync(
|
|
482
|
+
path.join(dir, '.consumption-stats.json'),
|
|
483
|
+
JSON.stringify({ date: '2026-06-02', dailyEvents: 10, searchHits: 5 }),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// With consumption stats → higher D6
|
|
487
|
+
const audit2 = new KnowledgeAudit({ baseDir: dir });
|
|
488
|
+
const report2 = audit2.run();
|
|
489
|
+
expect(report2.dimensions.flywheel.score).toBeGreaterThan(baseD6);
|
|
490
|
+
expect(report2.dimensions.flywheel.details.dailyConsumptionEvents).toBe(10);
|
|
491
|
+
|
|
492
|
+
fs.rmSync(dir, { recursive: true });
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ── Health score ────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
describe('health score', () => {
|
|
499
|
+
it('returns 100 for clean store', () => {
|
|
500
|
+
const dir = makeTmpDir();
|
|
501
|
+
setupStore(dir, [
|
|
502
|
+
{ id: 'G-001', maturity: 'verified', content: 'a'.repeat(100), title: 'Unique Title A' },
|
|
503
|
+
{ id: 'G-002', maturity: 'proven', content: 'b'.repeat(100), title: 'Unique Title B' },
|
|
504
|
+
]);
|
|
505
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
506
|
+
const report = audit.run();
|
|
507
|
+
expect(report.healthScore.before).toBe(100);
|
|
508
|
+
fs.rmSync(dir, { recursive: true });
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('decreases with critical issues', () => {
|
|
512
|
+
const dir = makeTmpDir();
|
|
513
|
+
setupStore(dir, [
|
|
514
|
+
{ id: 'G-003', maturity: 'proven', content: 'tiny', title: 'Test Entry' },
|
|
515
|
+
]);
|
|
516
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
517
|
+
const report = audit.run();
|
|
518
|
+
expect(report.healthScore.before).toBeLessThan(100);
|
|
519
|
+
fs.rmSync(dir, { recursive: true });
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// ── Auto-fix ────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
describe('auto-fix', () => {
|
|
526
|
+
it('archives test data entries', () => {
|
|
527
|
+
const dir = makeTmpDir();
|
|
528
|
+
const store = setupStore(dir, [
|
|
529
|
+
{ id: 'H-001', tags: ['test-scope-abc'], maturity: 'verified' },
|
|
530
|
+
]);
|
|
531
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
532
|
+
const report = audit.run({ autoFix: true });
|
|
533
|
+
expect(report.autoFixed).toBeGreaterThan(0);
|
|
534
|
+
expect(store.get('H-001')!.maturity).toBe('archived');
|
|
535
|
+
fs.rmSync(dir, { recursive: true });
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('demotes zero-content proven entries', () => {
|
|
539
|
+
const dir = makeTmpDir();
|
|
540
|
+
const store = setupStore(dir, [
|
|
541
|
+
{ id: 'H-002', maturity: 'proven', content: 'tiny', title: 'Normal Title' },
|
|
542
|
+
]);
|
|
543
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
544
|
+
audit.run({ autoFix: true });
|
|
545
|
+
expect(store.get('H-002')!.maturity).toBe('draft');
|
|
546
|
+
fs.rmSync(dir, { recursive: true });
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('flags short content entries with low_quality tag', () => {
|
|
550
|
+
const dir = makeTmpDir();
|
|
551
|
+
const store = setupStore(dir, [
|
|
552
|
+
{ id: 'H-003', maturity: 'draft', content: 'a'.repeat(30), title: 'Normal Title 2' },
|
|
553
|
+
]);
|
|
554
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
555
|
+
audit.run({ autoFix: true });
|
|
556
|
+
expect(store.get('H-003')!.tags).toContain('low_quality');
|
|
557
|
+
fs.rmSync(dir, { recursive: true });
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// ── run() report structure ─────────────────────────────
|
|
562
|
+
|
|
563
|
+
describe('run() report', () => {
|
|
564
|
+
it('returns correct report structure', () => {
|
|
565
|
+
const dir = makeTmpDir();
|
|
566
|
+
setupStore(dir, [{ id: 'I-001' }]);
|
|
567
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
568
|
+
const report = audit.run();
|
|
569
|
+
expect(report).toHaveProperty('timestamp');
|
|
570
|
+
expect(report).toHaveProperty('totalEntries');
|
|
571
|
+
expect(report).toHaveProperty('issues');
|
|
572
|
+
expect(report).toHaveProperty('summary');
|
|
573
|
+
expect(report).toHaveProperty('dimensions');
|
|
574
|
+
expect(report).toHaveProperty('autoFixed');
|
|
575
|
+
expect(report).toHaveProperty('healthScore');
|
|
576
|
+
expect(report.healthScore).toHaveProperty('before');
|
|
577
|
+
expect(report.healthScore).toHaveProperty('after');
|
|
578
|
+
expect(report.dimensions).toHaveProperty('structure');
|
|
579
|
+
expect(report.dimensions).toHaveProperty('content');
|
|
580
|
+
expect(report.dimensions).toHaveProperty('dedup');
|
|
581
|
+
expect(report.dimensions).toHaveProperty('maturity');
|
|
582
|
+
expect(report.dimensions).toHaveProperty('freshness');
|
|
583
|
+
expect(report.dimensions).toHaveProperty('flywheel');
|
|
584
|
+
fs.rmSync(dir, { recursive: true });
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('summary counts match issue counts', () => {
|
|
588
|
+
const dir = makeTmpDir();
|
|
589
|
+
setupStore(dir, [
|
|
590
|
+
{ id: 'I-002', title: 'Test Entry', maturity: 'proven', content: 'tiny' },
|
|
591
|
+
]);
|
|
592
|
+
const audit = new KnowledgeAudit({ baseDir: dir });
|
|
593
|
+
const report = audit.run();
|
|
594
|
+
const totalFromSummary = Object.values(report.summary).reduce((a, b) => a + b, 0);
|
|
595
|
+
expect(totalFromSummary).toBe(report.issues.length);
|
|
596
|
+
fs.rmSync(dir, { recursive: true });
|
|
597
|
+
});
|
|
598
|
+
});
|
|
@@ -72,6 +72,9 @@ describe('KnowledgeHealthScorer', () => {
|
|
|
72
72
|
applicablePhases: ['design'],
|
|
73
73
|
sourceReferences: [],
|
|
74
74
|
referencedBy: ['decision-1'],
|
|
75
|
+
executionResults: [],
|
|
76
|
+
consumptionMode: 'reference',
|
|
77
|
+
origin: 'agent',
|
|
75
78
|
};
|
|
76
79
|
store.save(entry);
|
|
77
80
|
|
|
@@ -100,6 +103,9 @@ describe('KnowledgeHealthScorer', () => {
|
|
|
100
103
|
applicablePhases: [],
|
|
101
104
|
sourceReferences: [],
|
|
102
105
|
referencedBy: [],
|
|
106
|
+
executionResults: [],
|
|
107
|
+
consumptionMode: 'reference',
|
|
108
|
+
origin: 'agent',
|
|
103
109
|
});
|
|
104
110
|
}
|
|
105
111
|
|
|
@@ -134,6 +140,9 @@ describe('KnowledgeHealthScorer', () => {
|
|
|
134
140
|
applicablePhases: [],
|
|
135
141
|
sourceReferences: [],
|
|
136
142
|
referencedBy: ['decision-ref'],
|
|
143
|
+
executionResults: [],
|
|
144
|
+
consumptionMode: 'reference',
|
|
145
|
+
origin: 'agent',
|
|
137
146
|
});
|
|
138
147
|
}
|
|
139
148
|
// Save healthy entries to keep ratios manageable (3 outdated out of ~56 = ~5.3%)
|
|
@@ -153,6 +162,9 @@ describe('KnowledgeHealthScorer', () => {
|
|
|
153
162
|
applicablePhases: [],
|
|
154
163
|
sourceReferences: [],
|
|
155
164
|
referencedBy: ['decision-ref'],
|
|
165
|
+
executionResults: [],
|
|
166
|
+
consumptionMode: 'reference',
|
|
167
|
+
origin: 'agent',
|
|
156
168
|
});
|
|
157
169
|
}
|
|
158
170
|
|
|
@@ -179,6 +191,9 @@ describe('KnowledgeHealthScorer', () => {
|
|
|
179
191
|
applicablePhases: [],
|
|
180
192
|
sourceReferences: [],
|
|
181
193
|
referencedBy: ['decision-ref'],
|
|
194
|
+
executionResults: [],
|
|
195
|
+
consumptionMode: 'reference',
|
|
196
|
+
origin: 'agent',
|
|
182
197
|
});
|
|
183
198
|
store.save({
|
|
184
199
|
id: 'dup-2',
|
|
@@ -195,6 +210,9 @@ describe('KnowledgeHealthScorer', () => {
|
|
|
195
210
|
applicablePhases: [],
|
|
196
211
|
sourceReferences: [],
|
|
197
212
|
referencedBy: ['decision-ref'],
|
|
213
|
+
executionResults: [],
|
|
214
|
+
consumptionMode: 'reference',
|
|
215
|
+
origin: 'agent',
|
|
198
216
|
});
|
|
199
217
|
|
|
200
218
|
const report = scorer.healthScore();
|
|
@@ -220,6 +238,9 @@ describe('KnowledgeHealthScorer', () => {
|
|
|
220
238
|
applicablePhases: [],
|
|
221
239
|
sourceReferences: [],
|
|
222
240
|
referencedBy: ['decision-ref'],
|
|
241
|
+
executionResults: [],
|
|
242
|
+
consumptionMode: 'reference',
|
|
243
|
+
origin: 'agent',
|
|
223
244
|
});
|
|
224
245
|
store.save({
|
|
225
246
|
id: 'draft-entry',
|
|
@@ -236,6 +257,9 @@ describe('KnowledgeHealthScorer', () => {
|
|
|
236
257
|
applicablePhases: [],
|
|
237
258
|
sourceReferences: [],
|
|
238
259
|
referencedBy: ['decision-ref'],
|
|
260
|
+
executionResults: [],
|
|
261
|
+
consumptionMode: 'reference',
|
|
262
|
+
origin: 'agent',
|
|
239
263
|
});
|
|
240
264
|
|
|
241
265
|
const report = scorer.healthScore();
|
|
@@ -262,6 +286,9 @@ describe('KnowledgeHealthScorer', () => {
|
|
|
262
286
|
applicablePhases: [],
|
|
263
287
|
sourceReferences: [],
|
|
264
288
|
referencedBy: [],
|
|
289
|
+
executionResults: [],
|
|
290
|
+
consumptionMode: 'reference',
|
|
291
|
+
origin: 'agent',
|
|
265
292
|
});
|
|
266
293
|
}
|
|
267
294
|
|