@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.
- 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
|
@@ -42,6 +42,9 @@ describe('KnowledgeLinter', () => {
|
|
|
42
42
|
applicablePhases: [],
|
|
43
43
|
sourceReferences: [],
|
|
44
44
|
referencedBy: [],
|
|
45
|
+
executionResults: [],
|
|
46
|
+
consumptionMode: 'reference',
|
|
47
|
+
origin: 'agent',
|
|
45
48
|
...overrides,
|
|
46
49
|
});
|
|
47
50
|
}
|
|
@@ -397,5 +400,53 @@ describe('KnowledgeLinter', () => {
|
|
|
397
400
|
const entry = store.get('old-1');
|
|
398
401
|
expect(entry?.maturity).toBe('archived');
|
|
399
402
|
});
|
|
403
|
+
|
|
404
|
+
it('A3: 应该归档重复条目,保留最新,合并 sourceReferences', () => {
|
|
405
|
+
// Create 3 entries with same title/type, different lastReferenced dates
|
|
406
|
+
saveEntry({
|
|
407
|
+
id: 'dup-old',
|
|
408
|
+
title: 'Duplicate Title',
|
|
409
|
+
type: 'decision',
|
|
410
|
+
lastReferenced: '2026-01-01T00:00:00.000Z',
|
|
411
|
+
sourceReferences: [{ workflow: 'old-wf', timestamp: '2026-01-01T00:00:00.000Z' }],
|
|
412
|
+
});
|
|
413
|
+
saveEntry({
|
|
414
|
+
id: 'dup-mid',
|
|
415
|
+
title: 'Duplicate Title',
|
|
416
|
+
type: 'decision',
|
|
417
|
+
lastReferenced: '2026-03-01T00:00:00.000Z',
|
|
418
|
+
sourceReferences: [{ workflow: 'mid-wf', timestamp: '2026-03-01T00:00:00.000Z' }],
|
|
419
|
+
});
|
|
420
|
+
saveEntry({
|
|
421
|
+
id: 'dup-new',
|
|
422
|
+
title: 'Duplicate Title',
|
|
423
|
+
type: 'decision',
|
|
424
|
+
lastReferenced: '2026-05-01T00:00:00.000Z',
|
|
425
|
+
sourceReferences: [{ workflow: 'new-wf', timestamp: '2026-05-01T00:00:00.000Z' }],
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const report = linter.run(true);
|
|
429
|
+
|
|
430
|
+
// Should have fixed 2 duplicates (archived old and mid)
|
|
431
|
+
const dupFixes = report.issues.filter(i => i.type === 'duplicate');
|
|
432
|
+
expect(dupFixes.length).toBe(3); // all 3 reported as duplicates
|
|
433
|
+
expect(report.fixed).toBeGreaterThanOrEqual(2);
|
|
434
|
+
|
|
435
|
+
// Newest should be kept
|
|
436
|
+
const kept = store.get('dup-new');
|
|
437
|
+
expect(kept).toBeDefined();
|
|
438
|
+
expect(kept!.maturity).not.toBe('archived');
|
|
439
|
+
|
|
440
|
+
// Old and mid should be archived
|
|
441
|
+
expect(store.get('dup-old')!.maturity).toBe('archived');
|
|
442
|
+
expect(store.get('dup-mid')!.maturity).toBe('archived');
|
|
443
|
+
|
|
444
|
+
// SourceReferences from archived entries should be merged into kept entry
|
|
445
|
+
expect(kept!.sourceReferences.length).toBeGreaterThanOrEqual(3);
|
|
446
|
+
const workflows = kept!.sourceReferences.map(r => r.workflow);
|
|
447
|
+
expect(workflows).toContain('old-wf');
|
|
448
|
+
expect(workflows).toContain('mid-wf');
|
|
449
|
+
expect(workflows).toContain('new-wf');
|
|
450
|
+
});
|
|
400
451
|
});
|
|
401
452
|
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Migration 测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
|
6
|
+
import { migrateKnowledgeEntries } from '../migration';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
describe('migrateKnowledgeEntries', () => {
|
|
11
|
+
const tempDir = path.join(process.cwd(), 'temp-test-migration');
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
try {
|
|
19
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
} catch {
|
|
21
|
+
// ignore
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
// Clean directory
|
|
27
|
+
for (const f of fs.readdirSync(tempDir)) {
|
|
28
|
+
fs.unlinkSync(path.join(tempDir, f));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function writeEntry(filename: string, meta: Record<string, unknown>, content = 'Body content') {
|
|
33
|
+
const frontmatter = Object.entries(meta)
|
|
34
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
35
|
+
.join('\n');
|
|
36
|
+
const raw = `---\n${frontmatter}\n---\n\n${content}`;
|
|
37
|
+
fs.writeFileSync(path.join(tempDir, filename), raw, 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readMeta(filename: string): Record<string, unknown> {
|
|
41
|
+
const raw = fs.readFileSync(path.join(tempDir, filename), 'utf-8');
|
|
42
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/)!;
|
|
43
|
+
return JSON.parse(JSON.stringify(eval(`(${match[1].replace(/(\w+):/g, '"$1":').replace(/'/g, '"')})`)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it('should add consumptionMode and origin to entries missing them', () => {
|
|
47
|
+
writeEntry('DEC-001.md', {
|
|
48
|
+
id: 'DEC-001', type: 'decision', title: 'Test', maturity: 'draft',
|
|
49
|
+
layer: 'project', created: '2026-05-01', lastReferenced: '',
|
|
50
|
+
contributors: [], projects: [], tags: [], applicablePhases: [],
|
|
51
|
+
sourceReferences: [], referencedBy: [], executionResults: [],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = migrateKnowledgeEntries(tempDir);
|
|
55
|
+
expect(result.total).toBe(1);
|
|
56
|
+
expect(result.migrated).toBe(1);
|
|
57
|
+
expect(result.skipped).toBe(0);
|
|
58
|
+
expect(result.errors).toHaveLength(0);
|
|
59
|
+
|
|
60
|
+
// Verify the file was updated
|
|
61
|
+
const raw = fs.readFileSync(path.join(tempDir, 'DEC-001.md'), 'utf-8');
|
|
62
|
+
expect(raw).toContain('consumptionMode');
|
|
63
|
+
expect(raw).toContain('origin');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should skip entries that already have both fields', () => {
|
|
67
|
+
writeEntry('DEC-002.md', {
|
|
68
|
+
id: 'DEC-002', type: 'decision', title: 'Test', maturity: 'draft',
|
|
69
|
+
layer: 'project', created: '2026-05-01', lastReferenced: '',
|
|
70
|
+
contributors: [], projects: [], tags: [], applicablePhases: [],
|
|
71
|
+
sourceReferences: [], referencedBy: [], executionResults: [],
|
|
72
|
+
consumptionMode: 'rule', origin: 'human',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = migrateKnowledgeEntries(tempDir);
|
|
76
|
+
expect(result.total).toBe(1);
|
|
77
|
+
expect(result.migrated).toBe(0);
|
|
78
|
+
expect(result.skipped).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle empty directory', () => {
|
|
82
|
+
const result = migrateKnowledgeEntries(path.join(tempDir, 'nonexistent'));
|
|
83
|
+
expect(result.total).toBe(0);
|
|
84
|
+
expect(result.migrated).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle non-markdown files gracefully', () => {
|
|
88
|
+
fs.writeFileSync(path.join(tempDir, 'index.json'), '{}', 'utf-8');
|
|
89
|
+
writeEntry('DEC-003.md', {
|
|
90
|
+
id: 'DEC-003', type: 'decision', title: 'Test', maturity: 'draft',
|
|
91
|
+
layer: 'project', created: '2026-05-01', lastReferenced: '',
|
|
92
|
+
contributors: [], projects: [], tags: [], applicablePhases: [],
|
|
93
|
+
sourceReferences: [], referencedBy: [], executionResults: [],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const result = migrateKnowledgeEntries(tempDir);
|
|
97
|
+
expect(result.total).toBe(1); // only .md files counted
|
|
98
|
+
expect(result.migrated).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should be idempotent', () => {
|
|
102
|
+
writeEntry('DEC-004.md', {
|
|
103
|
+
id: 'DEC-004', type: 'decision', title: 'Test', maturity: 'draft',
|
|
104
|
+
layer: 'project', created: '2026-05-01', lastReferenced: '',
|
|
105
|
+
contributors: [], projects: [], tags: [], applicablePhases: [],
|
|
106
|
+
sourceReferences: [], referencedBy: [], executionResults: [],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const first = migrateKnowledgeEntries(tempDir);
|
|
110
|
+
const second = migrateKnowledgeEntries(tempDir);
|
|
111
|
+
expect(first.migrated).toBe(1);
|
|
112
|
+
expect(second.migrated).toBe(0);
|
|
113
|
+
expect(second.skipped).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -29,6 +29,9 @@ describe('KnowledgeQuery', () => {
|
|
|
29
29
|
applicablePhases: [],
|
|
30
30
|
sourceReferences: [],
|
|
31
31
|
referencedBy: [],
|
|
32
|
+
executionResults: [],
|
|
33
|
+
consumptionMode: 'reference',
|
|
34
|
+
origin: 'agent',
|
|
32
35
|
...overrides,
|
|
33
36
|
});
|
|
34
37
|
|
|
@@ -182,4 +185,87 @@ describe('KnowledgeQuery', () => {
|
|
|
182
185
|
expect(result.fromCache).toBe(false);
|
|
183
186
|
});
|
|
184
187
|
});
|
|
188
|
+
|
|
189
|
+
describe('queryByMode', () => {
|
|
190
|
+
it('should filter by consumptionMode', () => {
|
|
191
|
+
store.save(makeEntry({ id: 'RUL-001', consumptionMode: 'rule', title: 'Rule' }));
|
|
192
|
+
store.save(makeEntry({ id: 'REF-001', consumptionMode: 'reference', title: 'Ref' }));
|
|
193
|
+
store.save(makeEntry({ id: 'SIG-001', consumptionMode: 'signal', title: 'Signal' }));
|
|
194
|
+
|
|
195
|
+
const rules = query.queryByMode('rule');
|
|
196
|
+
expect(rules).toHaveLength(1);
|
|
197
|
+
expect(rules[0].id).toBe('RUL-001');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should combine with additional filter', () => {
|
|
201
|
+
store.save(makeEntry({ id: 'REF-001', consumptionMode: 'reference', tags: ['arch'] }));
|
|
202
|
+
store.save(makeEntry({ id: 'REF-002', consumptionMode: 'reference', tags: ['api'] }));
|
|
203
|
+
|
|
204
|
+
const refs = query.queryByMode('reference', { tags: ['arch'] });
|
|
205
|
+
expect(refs).toHaveLength(1);
|
|
206
|
+
expect(refs[0].id).toBe('REF-001');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('consume', () => {
|
|
211
|
+
it('should return rules, references, and context', () => {
|
|
212
|
+
store.save(makeEntry({ id: 'RUL-001', consumptionMode: 'rule', title: 'Port Rule' }));
|
|
213
|
+
store.save(makeEntry({
|
|
214
|
+
id: 'REF-001', consumptionMode: 'reference', title: 'Architecture',
|
|
215
|
+
content: 'The API uses TypeScript and REST endpoints',
|
|
216
|
+
}));
|
|
217
|
+
store.save(makeEntry({
|
|
218
|
+
id: 'CTX-001', consumptionMode: 'context', title: 'Env',
|
|
219
|
+
applicablePhases: ['build'],
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
const result = query.consume({ requirementText: 'API TypeScript', phase: 'build' });
|
|
223
|
+
expect(result.rules).toHaveLength(1);
|
|
224
|
+
expect(result.rules[0].id).toBe('RUL-001');
|
|
225
|
+
expect(result.references.length).toBeGreaterThanOrEqual(1);
|
|
226
|
+
expect(result.context).toHaveLength(1);
|
|
227
|
+
expect(result.context[0].id).toBe('CTX-001');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should filter context by phase', () => {
|
|
231
|
+
store.save(makeEntry({
|
|
232
|
+
id: 'CTX-001', consumptionMode: 'context', title: 'Build Config',
|
|
233
|
+
applicablePhases: ['build'],
|
|
234
|
+
}));
|
|
235
|
+
store.save(makeEntry({
|
|
236
|
+
id: 'CTX-002', consumptionMode: 'context', title: 'Deploy Config',
|
|
237
|
+
applicablePhases: ['deploy'],
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
const result = query.consume({ phase: 'build' });
|
|
241
|
+
expect(result.context).toHaveLength(1);
|
|
242
|
+
expect(result.context[0].id).toBe('CTX-001');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should return empty references when no requirementText', () => {
|
|
246
|
+
store.save(makeEntry({ id: 'REF-001', consumptionMode: 'reference' }));
|
|
247
|
+
const result = query.consume({});
|
|
248
|
+
// With no text filter, returns up to 3
|
|
249
|
+
expect(result.references.length).toBeLessThanOrEqual(3);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('formatForPrompt', () => {
|
|
254
|
+
it('should add External Source prefix for external origin', () => {
|
|
255
|
+
const { KnowledgeQuery: KQ } = require('../query');
|
|
256
|
+
const entry = makeEntry({ origin: 'external', title: 'GitHub Doc', content: 'Architecture' });
|
|
257
|
+
const result = KQ.formatForPrompt(entry);
|
|
258
|
+
expect(result).toContain('[External Source');
|
|
259
|
+
expect(result).toContain('GitHub Doc');
|
|
260
|
+
expect(result).toContain('Architecture');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should not add prefix for agent origin', () => {
|
|
264
|
+
const { KnowledgeQuery: KQ } = require('../query');
|
|
265
|
+
const entry = makeEntry({ origin: 'agent', title: 'Internal', content: 'Details' });
|
|
266
|
+
const result = KQ.formatForPrompt(entry);
|
|
267
|
+
expect(result).not.toContain('[External Source');
|
|
268
|
+
expect(result).toBe('Internal: Details');
|
|
269
|
+
});
|
|
270
|
+
});
|
|
185
271
|
});
|
|
@@ -27,6 +27,9 @@ describe('KnowledgeStore', () => {
|
|
|
27
27
|
applicablePhases: ['ARCHITECT'],
|
|
28
28
|
sourceReferences: [],
|
|
29
29
|
referencedBy: [],
|
|
30
|
+
executionResults: [],
|
|
31
|
+
consumptionMode: 'reference',
|
|
32
|
+
origin: 'agent',
|
|
30
33
|
...overrides,
|
|
31
34
|
});
|
|
32
35
|
|
|
@@ -125,6 +128,23 @@ describe('KnowledgeStore', () => {
|
|
|
125
128
|
const list = store.list({ excludeArchived: false });
|
|
126
129
|
expect(list).toHaveLength(2);
|
|
127
130
|
});
|
|
131
|
+
|
|
132
|
+
it('should filter by consumptionMode', () => {
|
|
133
|
+
store.save(makeEntry({ id: 'DEC-001', consumptionMode: 'reference' }));
|
|
134
|
+
store.save(makeEntry({ id: 'DEC-002', consumptionMode: 'rule', title: 'Rule' }));
|
|
135
|
+
store.save(makeEntry({ id: 'DEC-003', consumptionMode: 'signal', title: 'Signal' }));
|
|
136
|
+
const list = store.list({ consumptionModes: ['rule', 'signal'] });
|
|
137
|
+
expect(list).toHaveLength(2);
|
|
138
|
+
expect(list.map(e => e.id).sort()).toEqual(['DEC-002', 'DEC-003']);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should filter by origin', () => {
|
|
142
|
+
store.save(makeEntry({ id: 'DEC-001', origin: 'agent' }));
|
|
143
|
+
store.save(makeEntry({ id: 'DEC-002', origin: 'human', title: 'Human' }));
|
|
144
|
+
const list = store.list({ origins: ['human'] });
|
|
145
|
+
expect(list).toHaveLength(1);
|
|
146
|
+
expect(list[0].origin).toBe('human');
|
|
147
|
+
});
|
|
128
148
|
});
|
|
129
149
|
|
|
130
150
|
describe('delete', () => {
|
|
@@ -164,6 +184,32 @@ describe('KnowledgeStore', () => {
|
|
|
164
184
|
});
|
|
165
185
|
|
|
166
186
|
describe('edge cases', () => {
|
|
187
|
+
it('should round-trip consumptionMode and origin through save/load', () => {
|
|
188
|
+
store.save(makeEntry({
|
|
189
|
+
id: 'DEC-001',
|
|
190
|
+
consumptionMode: 'rule',
|
|
191
|
+
origin: 'human',
|
|
192
|
+
fullContentPath: 'https://example.com/doc',
|
|
193
|
+
skillId: 'skill-001',
|
|
194
|
+
}));
|
|
195
|
+
const loaded = store.get('DEC-001');
|
|
196
|
+
expect(loaded?.consumptionMode).toBe('rule');
|
|
197
|
+
expect(loaded?.origin).toBe('human');
|
|
198
|
+
expect(loaded?.fullContentPath).toBe('https://example.com/doc');
|
|
199
|
+
expect(loaded?.skillId).toBe('skill-001');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should default consumptionMode to reference and origin to agent', () => {
|
|
203
|
+
store.save(makeEntry({ id: 'DEC-001' }));
|
|
204
|
+
const loaded = store.get('DEC-001');
|
|
205
|
+
expect(loaded?.consumptionMode).toBe('reference');
|
|
206
|
+
expect(loaded?.origin).toBe('agent');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('getBaseDir() returns configured baseDir', () => {
|
|
210
|
+
expect(store.getBaseDir()).toBe(tempDir);
|
|
211
|
+
});
|
|
212
|
+
|
|
167
213
|
it('should create directory if it does not exist', () => {
|
|
168
214
|
const newDir = path.join(process.cwd(), 'temp-test-knowledge-nested', 'sub', 'deep');
|
|
169
215
|
const s = new KnowledgeStore({ baseDir: newDir });
|
|
@@ -235,4 +281,165 @@ describe('KnowledgeStore', () => {
|
|
|
235
281
|
expect(retrieved!.decayAt).toBeUndefined();
|
|
236
282
|
});
|
|
237
283
|
});
|
|
284
|
+
|
|
285
|
+
describe('findFile precision (E3)', () => {
|
|
286
|
+
it('should not collide when one id is a suffix of another', () => {
|
|
287
|
+
// pitfall with id containing hyphen: file = pitfall-mppq8n44-1829.md
|
|
288
|
+
const pitfall = makeEntry({
|
|
289
|
+
id: 'PIT-mppq8n44-1829',
|
|
290
|
+
type: 'pitfall',
|
|
291
|
+
title: 'Pitfall Entry',
|
|
292
|
+
content: 'pitfall content',
|
|
293
|
+
});
|
|
294
|
+
// decision with numeric id that is a suffix: file = decision-1829.md
|
|
295
|
+
const decision = makeEntry({
|
|
296
|
+
id: '1829',
|
|
297
|
+
type: 'decision',
|
|
298
|
+
title: 'Decision Entry',
|
|
299
|
+
content: 'decision content',
|
|
300
|
+
});
|
|
301
|
+
store.save(pitfall);
|
|
302
|
+
store.save(decision);
|
|
303
|
+
|
|
304
|
+
// get('1829') must return the decision, not the pitfall
|
|
305
|
+
const retrieved = store.get('1829');
|
|
306
|
+
expect(retrieved).toBeDefined();
|
|
307
|
+
expect(retrieved!.id).toBe('1829');
|
|
308
|
+
expect(retrieved!.type).toBe('decision');
|
|
309
|
+
|
|
310
|
+
// get('PIT-mppq8n44-1829') must return the pitfall
|
|
311
|
+
const pit = store.get('PIT-mppq8n44-1829');
|
|
312
|
+
expect(pit).toBeDefined();
|
|
313
|
+
expect(pit!.id).toBe('PIT-mppq8n44-1829');
|
|
314
|
+
expect(pit!.type).toBe('pitfall');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should not collide when id contains type prefix of another entry', () => {
|
|
318
|
+
// model with id that contains "GUI" prefix: file = model-GUI-003.md
|
|
319
|
+
const model = makeEntry({
|
|
320
|
+
id: 'GUI-003',
|
|
321
|
+
type: 'model',
|
|
322
|
+
title: 'Model Entry',
|
|
323
|
+
content: 'model content',
|
|
324
|
+
});
|
|
325
|
+
// guideline with id "003": file = guideline-003.md
|
|
326
|
+
const guideline = makeEntry({
|
|
327
|
+
id: '003',
|
|
328
|
+
type: 'guideline',
|
|
329
|
+
title: 'Guideline Entry',
|
|
330
|
+
content: 'guideline content',
|
|
331
|
+
});
|
|
332
|
+
store.save(model);
|
|
333
|
+
store.save(guideline);
|
|
334
|
+
|
|
335
|
+
const retrieved = store.get('003');
|
|
336
|
+
expect(retrieved).toBeDefined();
|
|
337
|
+
expect(retrieved!.id).toBe('003');
|
|
338
|
+
expect(retrieved!.type).toBe('guideline');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should handle delete correctly with colliding ids', () => {
|
|
342
|
+
const pitfall = makeEntry({
|
|
343
|
+
id: 'PIT-mppq8n44-1829',
|
|
344
|
+
type: 'pitfall',
|
|
345
|
+
title: 'Pitfall Entry',
|
|
346
|
+
content: 'pitfall content',
|
|
347
|
+
});
|
|
348
|
+
const decision = makeEntry({
|
|
349
|
+
id: '1829',
|
|
350
|
+
type: 'decision',
|
|
351
|
+
title: 'Decision Entry',
|
|
352
|
+
content: 'decision content',
|
|
353
|
+
});
|
|
354
|
+
store.save(pitfall);
|
|
355
|
+
store.save(decision);
|
|
356
|
+
|
|
357
|
+
// delete('1829') should only delete the decision
|
|
358
|
+
expect(store.delete('1829')).toBe(true);
|
|
359
|
+
expect(store.get('1829')).toBeUndefined();
|
|
360
|
+
// pitfall should still exist
|
|
361
|
+
expect(store.get('PIT-mppq8n44-1829')).toBeDefined();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('snapshot', () => {
|
|
366
|
+
it('should create snapshot file in .snapshots/', () => {
|
|
367
|
+
store.save(makeEntry({ id: 'SNAP-001' }));
|
|
368
|
+
const snapPath = store.snapshot();
|
|
369
|
+
expect(fs.existsSync(snapPath)).toBe(true);
|
|
370
|
+
expect(snapPath).toContain('.snapshots/index-');
|
|
371
|
+
const content = JSON.parse(fs.readFileSync(snapPath, 'utf-8'));
|
|
372
|
+
expect(content.length).toBeGreaterThanOrEqual(1);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should overwrite existing snapshot for same date', () => {
|
|
376
|
+
store.save(makeEntry({ id: 'SNAP-001' }));
|
|
377
|
+
store.snapshot();
|
|
378
|
+
store.save(makeEntry({ id: 'SNAP-002', title: 'Second' }));
|
|
379
|
+
store.snapshot();
|
|
380
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
381
|
+
const snapPath = path.join(tempDir, '.snapshots', `index-${today}.json`);
|
|
382
|
+
const content = JSON.parse(fs.readFileSync(snapPath, 'utf-8'));
|
|
383
|
+
expect(content.some((e: any) => e.id === 'SNAP-002')).toBe(true);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should read snapshot by date', () => {
|
|
387
|
+
store.save(makeEntry({ id: 'SNAP-001' }));
|
|
388
|
+
store.snapshot();
|
|
389
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
390
|
+
const snapshot = store.getSnapshot(today);
|
|
391
|
+
expect(snapshot).toBeDefined();
|
|
392
|
+
expect(snapshot!.some(e => e.id === 'SNAP-001')).toBe(true);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should return undefined for non-existent snapshot date', () => {
|
|
396
|
+
expect(store.getSnapshot('2020-01-01')).toBeUndefined();
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('getSurvivalRate', () => {
|
|
401
|
+
it('should return undefined when no snapshot exists', () => {
|
|
402
|
+
expect(store.getSurvivalRate(30)).toBeUndefined();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should return 100% when all entries still active', () => {
|
|
406
|
+
store.save(makeEntry({ id: 'SUR-001' }));
|
|
407
|
+
store.save(makeEntry({ id: 'SUR-002' }));
|
|
408
|
+
// Create a snapshot with today's date, then check survival against 0 days ago
|
|
409
|
+
store.snapshot();
|
|
410
|
+
// Manually copy snapshot to look like 30 days ago
|
|
411
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
412
|
+
const pastDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
413
|
+
const snapDir = path.join(tempDir, '.snapshots');
|
|
414
|
+
fs.copyFileSync(
|
|
415
|
+
path.join(snapDir, `index-${today}.json`),
|
|
416
|
+
path.join(snapDir, `index-${pastDate}.json`),
|
|
417
|
+
);
|
|
418
|
+
const result = store.getSurvivalRate(30);
|
|
419
|
+
expect(result).toBeDefined();
|
|
420
|
+
expect(result!.rate).toBe(100);
|
|
421
|
+
expect(result!.survived).toBe(2);
|
|
422
|
+
expect(result!.total).toBe(2);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should count archived entries as not survived', () => {
|
|
426
|
+
store.save(makeEntry({ id: 'SUR-001' }));
|
|
427
|
+
store.save(makeEntry({ id: 'SUR-002' }));
|
|
428
|
+
store.snapshot();
|
|
429
|
+
// Archive one entry after snapshot
|
|
430
|
+
store.update('SUR-002', { maturity: 'archived' });
|
|
431
|
+
// Copy snapshot to 30 days ago
|
|
432
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
433
|
+
const pastDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
434
|
+
const snapDir = path.join(tempDir, '.snapshots');
|
|
435
|
+
fs.copyFileSync(
|
|
436
|
+
path.join(snapDir, `index-${today}.json`),
|
|
437
|
+
path.join(snapDir, `index-${pastDate}.json`),
|
|
438
|
+
);
|
|
439
|
+
const result = store.getSurvivalRate(30);
|
|
440
|
+
expect(result!.rate).toBe(50);
|
|
441
|
+
expect(result!.survived).toBe(1);
|
|
442
|
+
expect(result!.total).toBe(2);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
238
445
|
});
|