@dommaker/harness 0.13.2 → 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.
Files changed (133) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/harness-knowledge-check.js +38 -23
  3. package/bin/harness-knowledge-track.js +30 -4
  4. package/bin/harness.js +24 -1
  5. package/dist/cli/commands/diagnose.d.ts +16 -0
  6. package/dist/cli/commands/diagnose.d.ts.map +1 -0
  7. package/dist/cli/commands/diagnose.js +195 -0
  8. package/dist/cli/commands/diagnose.js.map +1 -0
  9. package/dist/cli/commands/index.d.ts +1 -1
  10. package/dist/cli/commands/index.d.ts.map +1 -1
  11. package/dist/cli/commands/index.js +4 -1
  12. package/dist/cli/commands/index.js.map +1 -1
  13. package/dist/cli/commands/knowledge.d.ts +21 -0
  14. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  15. package/dist/cli/commands/knowledge.js +155 -4
  16. package/dist/cli/commands/knowledge.js.map +1 -1
  17. package/dist/cli/commands/propose.d.ts +18 -0
  18. package/dist/cli/commands/propose.d.ts.map +1 -0
  19. package/dist/cli/commands/propose.js +331 -0
  20. package/dist/cli/commands/propose.js.map +1 -0
  21. package/dist/cli/commands/traces.d.ts +15 -0
  22. package/dist/cli/commands/traces.d.ts.map +1 -0
  23. package/dist/cli/commands/traces.js +167 -0
  24. package/dist/cli/commands/traces.js.map +1 -0
  25. package/dist/constraints/definitions.d.ts +39 -0
  26. package/dist/constraints/definitions.d.ts.map +1 -0
  27. package/dist/constraints/definitions.js +71 -0
  28. package/dist/constraints/definitions.js.map +1 -0
  29. package/dist/constraints/quality.d.ts +11 -0
  30. package/dist/constraints/quality.d.ts.map +1 -0
  31. package/dist/constraints/quality.js +257 -0
  32. package/dist/constraints/quality.js.map +1 -0
  33. package/dist/constraints/safety.d.ts +13 -0
  34. package/dist/constraints/safety.d.ts.map +1 -0
  35. package/dist/constraints/safety.js +103 -0
  36. package/dist/constraints/safety.js.map +1 -0
  37. package/dist/core/constraints/definitions.d.ts.map +1 -1
  38. package/dist/core/constraints/definitions.js +4 -2
  39. package/dist/core/constraints/definitions.js.map +1 -1
  40. package/dist/extensions/long-running/constraints.d.ts +38 -0
  41. package/dist/extensions/long-running/constraints.d.ts.map +1 -0
  42. package/dist/extensions/long-running/constraints.js +153 -0
  43. package/dist/extensions/long-running/constraints.js.map +1 -0
  44. package/dist/extensions/long-running/index.d.ts +30 -0
  45. package/dist/extensions/long-running/index.d.ts.map +1 -0
  46. package/dist/extensions/long-running/index.js +50 -0
  47. package/dist/extensions/long-running/index.js.map +1 -0
  48. package/dist/extensions/long-running/types.d.ts +154 -0
  49. package/dist/extensions/long-running/types.d.ts.map +1 -0
  50. package/dist/extensions/long-running/types.js +9 -0
  51. package/dist/extensions/long-running/types.js.map +1 -0
  52. package/dist/index.d.ts +3 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +6 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/knowledge/audit.d.ts +76 -0
  57. package/dist/knowledge/audit.d.ts.map +1 -0
  58. package/dist/knowledge/audit.js +566 -0
  59. package/dist/knowledge/audit.js.map +1 -0
  60. package/dist/knowledge/import.d.ts.map +1 -1
  61. package/dist/knowledge/import.js +3 -0
  62. package/dist/knowledge/import.js.map +1 -1
  63. package/dist/knowledge/index.d.ts +5 -1
  64. package/dist/knowledge/index.d.ts.map +1 -1
  65. package/dist/knowledge/index.js +6 -1
  66. package/dist/knowledge/index.js.map +1 -1
  67. package/dist/knowledge/ingest.d.ts +27 -0
  68. package/dist/knowledge/ingest.d.ts.map +1 -1
  69. package/dist/knowledge/ingest.js +150 -5
  70. package/dist/knowledge/ingest.js.map +1 -1
  71. package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
  72. package/dist/knowledge/lifecycle-hooks.js +3 -0
  73. package/dist/knowledge/lifecycle-hooks.js.map +1 -1
  74. package/dist/knowledge/lifecycle.d.ts +59 -7
  75. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  76. package/dist/knowledge/lifecycle.js +213 -52
  77. package/dist/knowledge/lifecycle.js.map +1 -1
  78. package/dist/knowledge/lint.d.ts.map +1 -1
  79. package/dist/knowledge/lint.js +43 -2
  80. package/dist/knowledge/lint.js.map +1 -1
  81. package/dist/knowledge/migration.d.ts +25 -0
  82. package/dist/knowledge/migration.d.ts.map +1 -0
  83. package/dist/knowledge/migration.js +96 -0
  84. package/dist/knowledge/migration.js.map +1 -0
  85. package/dist/knowledge/query.d.ts +29 -1
  86. package/dist/knowledge/query.d.ts.map +1 -1
  87. package/dist/knowledge/query.js +58 -0
  88. package/dist/knowledge/query.js.map +1 -1
  89. package/dist/knowledge/store.d.ts +21 -1
  90. package/dist/knowledge/store.d.ts.map +1 -1
  91. package/dist/knowledge/store.js +103 -6
  92. package/dist/knowledge/store.js.map +1 -1
  93. package/dist/knowledge/types.d.ts +29 -1
  94. package/dist/knowledge/types.d.ts.map +1 -1
  95. package/dist/knowledge/types.js +1 -0
  96. package/dist/knowledge/types.js.map +1 -1
  97. package/dist/presets/long-running.d.ts +33 -0
  98. package/dist/presets/long-running.d.ts.map +1 -0
  99. package/dist/presets/long-running.js +52 -0
  100. package/dist/presets/long-running.js.map +1 -0
  101. package/package.json +1 -1
  102. package/src/cli/commands/__tests__/knowledge.test.ts +167 -0
  103. package/src/cli/commands/index.ts +1 -1
  104. package/src/cli/commands/knowledge.ts +175 -4
  105. package/src/context/__tests__/knowledge-injector.test.ts +3 -0
  106. package/src/core/constraints/definitions.ts +4 -2
  107. package/src/dashboard/__tests__/data.test.ts +3 -0
  108. package/src/dashboard/__tests__/stats.test.ts +3 -0
  109. package/src/index.ts +3 -1
  110. package/src/knowledge/CONTEXT.md +29 -7
  111. package/src/knowledge/__tests__/audit.test.ts +598 -0
  112. package/src/knowledge/__tests__/doctor.test.ts +27 -0
  113. package/src/knowledge/__tests__/ingest.test.ts +319 -0
  114. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +3 -0
  115. package/src/knowledge/__tests__/lifecycle.test.ts +655 -2
  116. package/src/knowledge/__tests__/lint.test.ts +51 -0
  117. package/src/knowledge/__tests__/migration.test.ts +115 -0
  118. package/src/knowledge/__tests__/query.test.ts +86 -0
  119. package/src/knowledge/__tests__/reference-tracker.test.ts +3 -0
  120. package/src/knowledge/__tests__/store.test.ts +207 -0
  121. package/src/knowledge/audit.ts +640 -0
  122. package/src/knowledge/import.ts +3 -0
  123. package/src/knowledge/index.ts +5 -1
  124. package/src/knowledge/ingest.ts +169 -5
  125. package/src/knowledge/lifecycle-hooks.ts +3 -0
  126. package/src/knowledge/lifecycle.ts +218 -49
  127. package/src/knowledge/lint.ts +43 -2
  128. package/src/knowledge/migration.ts +73 -0
  129. package/src/knowledge/query.ts +71 -0
  130. package/src/knowledge/store.ts +100 -6
  131. package/src/knowledge/types.ts +33 -1
  132. package/src/monitoring/__tests__/knowledge-doctor.test.ts +3 -0
  133. 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