@soleri/core 7.0.0 → 8.0.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/dist/agency/agency-manager.d.ts +27 -1
- package/dist/agency/agency-manager.d.ts.map +1 -1
- package/dist/agency/agency-manager.js +180 -9
- package/dist/agency/agency-manager.js.map +1 -1
- package/dist/agency/default-rules.d.ts +7 -0
- package/dist/agency/default-rules.d.ts.map +1 -0
- package/dist/agency/default-rules.js +79 -0
- package/dist/agency/default-rules.js.map +1 -0
- package/dist/agency/types.d.ts +48 -0
- package/dist/agency/types.d.ts.map +1 -1
- package/dist/brain/brain.d.ts +17 -2
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +118 -8
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/knowledge-synthesizer.d.ts +37 -0
- package/dist/brain/knowledge-synthesizer.d.ts.map +1 -0
- package/dist/brain/knowledge-synthesizer.js +161 -0
- package/dist/brain/knowledge-synthesizer.js.map +1 -0
- package/dist/brain/learning-radar.d.ts +96 -0
- package/dist/brain/learning-radar.d.ts.map +1 -0
- package/dist/brain/learning-radar.js +202 -0
- package/dist/brain/learning-radar.js.map +1 -0
- package/dist/brain/types.d.ts +15 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/context/context-engine.d.ts.map +1 -1
- package/dist/context/context-engine.js +82 -17
- package/dist/context/context-engine.js.map +1 -1
- package/dist/context/types.d.ts +5 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/control/intent-router.d.ts +12 -1
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +68 -0
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +17 -0
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/classifier.d.ts +18 -0
- package/dist/curator/classifier.d.ts.map +1 -0
- package/dist/curator/classifier.js +61 -0
- package/dist/curator/classifier.js.map +1 -0
- package/dist/curator/quality-gate.d.ts +29 -0
- package/dist/curator/quality-gate.d.ts.map +1 -0
- package/dist/curator/quality-gate.js +88 -0
- package/dist/curator/quality-gate.js.map +1 -0
- package/dist/engine/bin/soleri-engine.js +1 -0
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/events/event-bus.d.ts +30 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +51 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/flows/chain-runner.d.ts +46 -0
- package/dist/flows/chain-runner.d.ts.map +1 -0
- package/dist/flows/chain-runner.js +271 -0
- package/dist/flows/chain-runner.js.map +1 -0
- package/dist/flows/chain-types.d.ts +103 -0
- package/dist/flows/chain-types.d.ts.map +1 -0
- package/dist/flows/chain-types.js +23 -0
- package/dist/flows/chain-types.js.map +1 -0
- package/dist/health/doctor-checks.d.ts +15 -0
- package/dist/health/doctor-checks.d.ts.map +1 -0
- package/dist/health/doctor-checks.js +98 -0
- package/dist/health/doctor-checks.js.map +1 -0
- package/dist/intake/text-ingester.d.ts +52 -0
- package/dist/intake/text-ingester.d.ts.map +1 -0
- package/dist/intake/text-ingester.js +181 -0
- package/dist/intake/text-ingester.js.map +1 -0
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +37 -1
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/llm/oauth-discovery.d.ts +26 -0
- package/dist/llm/oauth-discovery.d.ts.map +1 -0
- package/dist/llm/oauth-discovery.js +149 -0
- package/dist/llm/oauth-discovery.js.map +1 -0
- package/dist/planning/evidence-collector.d.ts +41 -0
- package/dist/planning/evidence-collector.d.ts.map +1 -0
- package/dist/planning/evidence-collector.js +194 -0
- package/dist/planning/evidence-collector.js.map +1 -0
- package/dist/planning/planner.d.ts +4 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +11 -0
- package/dist/planning/planner.js.map +1 -1
- package/dist/queue/job-queue.d.ts +92 -0
- package/dist/queue/job-queue.d.ts.map +1 -0
- package/dist/queue/job-queue.js +180 -0
- package/dist/queue/job-queue.js.map +1 -0
- package/dist/queue/pipeline-runner.d.ts +62 -0
- package/dist/queue/pipeline-runner.d.ts.map +1 -0
- package/dist/queue/pipeline-runner.js +126 -0
- package/dist/queue/pipeline-runner.js.map +1 -0
- package/dist/runtime/admin-setup-ops.d.ts +20 -0
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -0
- package/dist/runtime/admin-setup-ops.js +583 -0
- package/dist/runtime/admin-setup-ops.js.map +1 -0
- package/dist/runtime/chain-ops.d.ts +9 -0
- package/dist/runtime/chain-ops.d.ts.map +1 -0
- package/dist/runtime/chain-ops.js +107 -0
- package/dist/runtime/chain-ops.js.map +1 -0
- package/dist/runtime/claude-md-helpers.d.ts +65 -0
- package/dist/runtime/claude-md-helpers.d.ts.map +1 -0
- package/dist/runtime/claude-md-helpers.js +173 -0
- package/dist/runtime/claude-md-helpers.js.map +1 -0
- package/dist/runtime/curator-extra-ops.d.ts +3 -2
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +81 -3
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/facades/admin-facade.d.ts.map +1 -1
- package/dist/runtime/facades/admin-facade.js +4 -0
- package/dist/runtime/facades/admin-facade.js.map +1 -1
- package/dist/runtime/facades/agency-facade.d.ts.map +1 -1
- package/dist/runtime/facades/agency-facade.js +64 -0
- package/dist/runtime/facades/agency-facade.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +122 -1
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +42 -0
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +20 -2
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +2 -0
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
- package/dist/runtime/facades/vault-facade.js +25 -5
- package/dist/runtime/facades/vault-facade.js.map +1 -1
- package/dist/runtime/intake-ops.d.ts +7 -5
- package/dist/runtime/intake-ops.d.ts.map +1 -1
- package/dist/runtime/intake-ops.js +98 -5
- package/dist/runtime/intake-ops.js.map +1 -1
- package/dist/runtime/memory-extra-ops.d.ts +6 -3
- package/dist/runtime/memory-extra-ops.d.ts.map +1 -1
- package/dist/runtime/memory-extra-ops.js +292 -4
- package/dist/runtime/memory-extra-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +85 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/playbook-ops.js +1 -1
- package/dist/runtime/playbook-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +143 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts +23 -0
- package/dist/runtime/session-briefing.d.ts.map +1 -0
- package/dist/runtime/session-briefing.js +140 -0
- package/dist/runtime/session-briefing.js.map +1 -0
- package/dist/runtime/types.d.ts +23 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.js +1 -3
- package/dist/runtime/vault-linking-ops.js.map +1 -1
- package/dist/vault/vault.d.ts +25 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +67 -3
- package/dist/vault/vault.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/admin-setup-ops.test.ts +355 -0
- package/src/__tests__/async-infrastructure.test.ts +307 -0
- package/src/__tests__/cognee-client-gaps.test.ts +6 -2
- package/src/__tests__/cognee-hybrid-search.test.ts +49 -35
- package/src/__tests__/cognee-sync-manager-deep.test.ts +89 -65
- package/src/__tests__/curator-extra-ops.test.ts +6 -2
- package/src/__tests__/curator-pipeline-e2e.test.ts +358 -0
- package/src/__tests__/memory-extra-ops.test.ts +2 -2
- package/src/__tests__/planning-extra-ops.test.ts +2 -2
- package/src/__tests__/second-brain-features.test.ts +583 -0
- package/src/agency/agency-manager.ts +217 -9
- package/src/agency/default-rules.ts +83 -0
- package/src/agency/types.ts +61 -0
- package/src/brain/brain.ts +110 -8
- package/src/brain/knowledge-synthesizer.ts +218 -0
- package/src/brain/learning-radar.ts +340 -0
- package/src/brain/types.ts +16 -0
- package/src/context/context-engine.ts +114 -15
- package/src/context/types.ts +5 -0
- package/src/control/intent-router.ts +107 -0
- package/src/control/types.ts +10 -0
- package/src/curator/classifier.ts +88 -0
- package/src/curator/quality-gate.ts +129 -0
- package/src/engine/bin/soleri-engine.ts +1 -0
- package/src/events/event-bus.ts +58 -0
- package/src/flows/chain-runner.ts +369 -0
- package/src/flows/chain-types.ts +57 -0
- package/src/health/doctor-checks.ts +115 -0
- package/src/intake/text-ingester.ts +234 -0
- package/src/llm/llm-client.ts +38 -1
- package/src/llm/oauth-discovery.ts +169 -0
- package/src/planning/evidence-collector.ts +247 -0
- package/src/planning/planner.ts +11 -0
- package/src/queue/job-queue.ts +281 -0
- package/src/queue/pipeline-runner.ts +149 -0
- package/src/runtime/admin-setup-ops.ts +664 -0
- package/src/runtime/chain-ops.ts +121 -0
- package/src/runtime/claude-md-helpers.ts +236 -0
- package/src/runtime/curator-extra-ops.ts +86 -3
- package/src/runtime/facades/admin-facade.ts +4 -0
- package/src/runtime/facades/agency-facade.ts +68 -0
- package/src/runtime/facades/brain-facade.ts +142 -1
- package/src/runtime/facades/control-facade.ts +45 -0
- package/src/runtime/facades/memory-facade.ts +20 -2
- package/src/runtime/facades/plan-facade.ts +2 -0
- package/src/runtime/facades/vault-facade.ts +28 -5
- package/src/runtime/intake-ops.ts +107 -5
- package/src/runtime/memory-extra-ops.ts +312 -4
- package/src/runtime/planning-extra-ops.ts +94 -0
- package/src/runtime/playbook-ops.ts +1 -1
- package/src/runtime/runtime.ts +138 -2
- package/src/runtime/session-briefing.ts +161 -0
- package/src/runtime/types.ts +23 -0
- package/src/runtime/vault-linking-ops.ts +1 -3
- package/src/vault/vault.ts +79 -4
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for Second Brain features (#39 milestone).
|
|
3
|
+
*
|
|
4
|
+
* Tests all 8 features end-to-end using a real in-memory vault + brain:
|
|
5
|
+
* 1. Two-pass vault retrieval (scan → load)
|
|
6
|
+
* 2. Session briefing
|
|
7
|
+
* 3. Evidence-based reconciliation
|
|
8
|
+
* 4. Routing feedback loop
|
|
9
|
+
* 5. Ambient learning radar
|
|
10
|
+
* 6. External knowledge ingestion (text)
|
|
11
|
+
* 7. Content synthesis
|
|
12
|
+
* 8. Composable skill chains
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { Vault } from '../vault/vault.js';
|
|
20
|
+
import { Brain } from '../brain/brain.js';
|
|
21
|
+
import { BrainIntelligence } from '../brain/intelligence.js';
|
|
22
|
+
import { Planner } from '../planning/planner.js';
|
|
23
|
+
import { Curator } from '../curator/curator.js';
|
|
24
|
+
import { IntentRouter } from '../control/intent-router.js';
|
|
25
|
+
import { LearningRadar } from '../brain/learning-radar.js';
|
|
26
|
+
import { KnowledgeSynthesizer } from '../brain/knowledge-synthesizer.js';
|
|
27
|
+
import { ChainRunner } from '../flows/chain-runner.js';
|
|
28
|
+
import { TextIngester } from '../intake/text-ingester.js';
|
|
29
|
+
import { collectGitEvidence } from '../planning/evidence-collector.js';
|
|
30
|
+
import type { IntelligenceEntry } from '../intelligence/types.js';
|
|
31
|
+
|
|
32
|
+
// ─── Shared Setup ────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
let vault: Vault;
|
|
35
|
+
let brain: Brain;
|
|
36
|
+
let brainIntelligence: BrainIntelligence;
|
|
37
|
+
let planner: Planner;
|
|
38
|
+
let curator: Curator;
|
|
39
|
+
let intentRouter: IntentRouter;
|
|
40
|
+
let learningRadar: LearningRadar;
|
|
41
|
+
let synthesizer: KnowledgeSynthesizer;
|
|
42
|
+
let chainRunner: ChainRunner;
|
|
43
|
+
let textIngester: TextIngester;
|
|
44
|
+
let tempDir: string;
|
|
45
|
+
|
|
46
|
+
const SEED: IntelligenceEntry[] = [
|
|
47
|
+
{
|
|
48
|
+
id: 'pattern-retry',
|
|
49
|
+
type: 'pattern',
|
|
50
|
+
domain: 'architecture',
|
|
51
|
+
title: 'Retry with Exponential Backoff',
|
|
52
|
+
severity: 'critical',
|
|
53
|
+
description: 'Always use exponential backoff when retrying failed network requests.',
|
|
54
|
+
tags: ['networking', 'retry', 'resilience'],
|
|
55
|
+
context: 'HTTP clients, API gateways.',
|
|
56
|
+
why: 'Prevents thundering herd on service recovery.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'anti-pattern-polling',
|
|
60
|
+
type: 'anti-pattern',
|
|
61
|
+
domain: 'architecture',
|
|
62
|
+
title: 'Polling Without Timeout',
|
|
63
|
+
severity: 'critical',
|
|
64
|
+
description: 'Never poll an external service without a maximum timeout.',
|
|
65
|
+
tags: ['networking', 'polling', 'timeout'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'pattern-semantic-tokens',
|
|
69
|
+
type: 'pattern',
|
|
70
|
+
domain: 'design',
|
|
71
|
+
title: 'Semantic Token Priority',
|
|
72
|
+
severity: 'warning',
|
|
73
|
+
description: 'Use semantic tokens over primitive tokens for maintainability.',
|
|
74
|
+
tags: ['tokens', 'design-system', 'css'],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'pattern-fts5',
|
|
78
|
+
type: 'pattern',
|
|
79
|
+
domain: 'architecture',
|
|
80
|
+
title: 'FTS5 Full-Text Search',
|
|
81
|
+
severity: 'suggestion',
|
|
82
|
+
description: 'Use SQLite FTS5 with porter tokenizer for text search in the vault.',
|
|
83
|
+
tags: ['search', 'sqlite', 'fts5'],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'pattern-tdd',
|
|
87
|
+
type: 'pattern',
|
|
88
|
+
domain: 'testing',
|
|
89
|
+
title: 'Test-Driven Development',
|
|
90
|
+
severity: 'warning',
|
|
91
|
+
description: 'Write tests before implementation. RED → GREEN → REFACTOR cycle.',
|
|
92
|
+
tags: ['testing', 'tdd', 'quality'],
|
|
93
|
+
why: 'Catches design issues early and produces better APIs.',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
beforeAll(() => {
|
|
98
|
+
tempDir = mkdtempSync(join(tmpdir(), 'soleri-brain-test-'));
|
|
99
|
+
vault = new Vault(':memory:');
|
|
100
|
+
vault.seed(SEED);
|
|
101
|
+
brain = new Brain(vault);
|
|
102
|
+
brainIntelligence = new BrainIntelligence(vault, brain);
|
|
103
|
+
planner = new Planner(join(tempDir, 'plans.json'));
|
|
104
|
+
curator = new Curator(vault);
|
|
105
|
+
intentRouter = new IntentRouter(vault);
|
|
106
|
+
learningRadar = new LearningRadar(vault, brain);
|
|
107
|
+
synthesizer = new KnowledgeSynthesizer(brain, null); // No LLM — tests raw fallback
|
|
108
|
+
chainRunner = new ChainRunner(vault.getProvider());
|
|
109
|
+
textIngester = new TextIngester(vault, null); // No LLM — tests graceful degradation
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
afterAll(() => {
|
|
113
|
+
vault.close();
|
|
114
|
+
try {
|
|
115
|
+
rmSync(tempDir, { recursive: true });
|
|
116
|
+
} catch {
|
|
117
|
+
/* cleanup best-effort */
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ─── 1. Two-Pass Vault Retrieval ─────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe('Two-pass vault retrieval (#205)', () => {
|
|
124
|
+
it('scanSearch returns lightweight results without full entry body', async () => {
|
|
125
|
+
const results = await brain.scanSearch('retry network');
|
|
126
|
+
expect(results.length).toBeGreaterThan(0);
|
|
127
|
+
|
|
128
|
+
const first = results[0];
|
|
129
|
+
expect(first.id).toBeDefined();
|
|
130
|
+
expect(first.title).toBeDefined();
|
|
131
|
+
expect(first.score).toBeGreaterThan(0);
|
|
132
|
+
expect(first.snippet).toBeDefined();
|
|
133
|
+
expect(first.tokenEstimate).toBeGreaterThan(0);
|
|
134
|
+
// Scan results should NOT have full entry fields
|
|
135
|
+
expect((first as Record<string, unknown>).description).toBeUndefined();
|
|
136
|
+
expect((first as Record<string, unknown>).context).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('loadEntries returns full entries by ID', () => {
|
|
140
|
+
const entries = brain.loadEntries(['pattern-retry', 'pattern-fts5']);
|
|
141
|
+
expect(entries.length).toBe(2);
|
|
142
|
+
expect(entries[0].description).toBeDefined();
|
|
143
|
+
expect(entries[0].tags).toBeDefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('loadEntries skips unknown IDs gracefully', () => {
|
|
147
|
+
const entries = brain.loadEntries(['pattern-retry', 'nonexistent-id']);
|
|
148
|
+
expect(entries.length).toBe(1);
|
|
149
|
+
expect(entries[0].id).toBe('pattern-retry');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('scanSearch snippet is truncated to 120 chars', async () => {
|
|
153
|
+
const results = await brain.scanSearch('retry');
|
|
154
|
+
for (const r of results) {
|
|
155
|
+
expect(r.snippet.length).toBeLessThanOrEqual(123); // 120 + '...'
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── 2. Session Briefing ─────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe('Session briefing (#202)', () => {
|
|
163
|
+
// Session briefing is an op — tested via the module imports directly
|
|
164
|
+
it('brainIntelligence.listSessions returns sessions', () => {
|
|
165
|
+
const sessions = brainIntelligence.listSessions({ limit: 5 });
|
|
166
|
+
// May be empty in a fresh vault, but shouldn't throw
|
|
167
|
+
expect(Array.isArray(sessions)).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('planner.list returns plans array', () => {
|
|
171
|
+
const plans = planner.list();
|
|
172
|
+
expect(Array.isArray(plans)).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('vault.getRecent returns recent entries', () => {
|
|
176
|
+
const recent = vault.getRecent(5);
|
|
177
|
+
expect(recent.length).toBeGreaterThan(0);
|
|
178
|
+
expect(recent[0].title).toBeDefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('curator.healthAudit returns a score', () => {
|
|
182
|
+
const audit = curator.healthAudit();
|
|
183
|
+
expect(audit.score).toBeGreaterThanOrEqual(0);
|
|
184
|
+
expect(audit.score).toBeLessThanOrEqual(100);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ─── 3. Evidence-Based Reconciliation ────────────────────────────────
|
|
189
|
+
|
|
190
|
+
describe('Evidence-based reconciliation (#206)', () => {
|
|
191
|
+
it('collectGitEvidence handles non-git directory gracefully', () => {
|
|
192
|
+
const plan = planner.create({
|
|
193
|
+
objective: 'Test plan',
|
|
194
|
+
scope: 'test',
|
|
195
|
+
decisions: [],
|
|
196
|
+
tasks: [{ title: 'Add retry logic', description: 'Implement retry' }],
|
|
197
|
+
});
|
|
198
|
+
planner.approve(plan.id);
|
|
199
|
+
planner.startExecution(plan.id);
|
|
200
|
+
|
|
201
|
+
// /tmp is not a git repo — should return empty evidence, not crash
|
|
202
|
+
const evidence = collectGitEvidence(plan, '/tmp');
|
|
203
|
+
expect(evidence.planId).toBe(plan.id);
|
|
204
|
+
expect(evidence.accuracy).toBeDefined();
|
|
205
|
+
expect(Array.isArray(evidence.taskEvidence)).toBe(true);
|
|
206
|
+
expect(Array.isArray(evidence.unplannedChanges)).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('reports MISSING verdict when no git changes match tasks', () => {
|
|
210
|
+
const plan = planner.create({
|
|
211
|
+
objective: 'Another test',
|
|
212
|
+
scope: 'test',
|
|
213
|
+
decisions: [],
|
|
214
|
+
tasks: [
|
|
215
|
+
{ title: 'Create FooWidget component', description: 'New widget' },
|
|
216
|
+
{ title: 'Add unit tests for FooWidget', description: 'Tests' },
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
planner.approve(plan.id);
|
|
220
|
+
planner.startExecution(plan.id);
|
|
221
|
+
|
|
222
|
+
const evidence = collectGitEvidence(plan, '/tmp');
|
|
223
|
+
// No git changes in /tmp → all tasks should be MISSING
|
|
224
|
+
for (const te of evidence.taskEvidence) {
|
|
225
|
+
expect(['MISSING', 'SKIPPED']).toContain(te.verdict);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ─── 4. Routing Feedback Loop ────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
describe('Routing feedback loop (#209)', () => {
|
|
233
|
+
it('records routing feedback', () => {
|
|
234
|
+
const result = intentRouter.recordRoutingFeedback({
|
|
235
|
+
initialIntent: 'build',
|
|
236
|
+
actualIntent: 'fix',
|
|
237
|
+
confidence: 0.72,
|
|
238
|
+
correction: true,
|
|
239
|
+
});
|
|
240
|
+
expect(result.recorded).toBe(true);
|
|
241
|
+
expect(result.id).toBeGreaterThan(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('records correct routing feedback', () => {
|
|
245
|
+
intentRouter.recordRoutingFeedback({
|
|
246
|
+
initialIntent: 'build',
|
|
247
|
+
actualIntent: 'build',
|
|
248
|
+
confidence: 0.85,
|
|
249
|
+
correction: false,
|
|
250
|
+
});
|
|
251
|
+
intentRouter.recordRoutingFeedback({
|
|
252
|
+
initialIntent: 'fix',
|
|
253
|
+
actualIntent: 'fix',
|
|
254
|
+
confidence: 0.9,
|
|
255
|
+
correction: false,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('getRoutingAccuracy returns accuracy report', () => {
|
|
260
|
+
const report = intentRouter.getRoutingAccuracy(30);
|
|
261
|
+
expect(report.total).toBeGreaterThanOrEqual(3);
|
|
262
|
+
expect(report.accuracy).toBeGreaterThanOrEqual(0);
|
|
263
|
+
expect(report.accuracy).toBeLessThanOrEqual(100);
|
|
264
|
+
expect(report.corrections).toBeGreaterThanOrEqual(1);
|
|
265
|
+
expect(Array.isArray(report.commonMisroutes)).toBe(true);
|
|
266
|
+
expect(typeof report.confidenceCalibration).toBe('object');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('tracks common misroutes', () => {
|
|
270
|
+
const report = intentRouter.getRoutingAccuracy(30);
|
|
271
|
+
const misroute = report.commonMisroutes.find((m) => m.from === 'build' && m.to === 'fix');
|
|
272
|
+
expect(misroute).toBeDefined();
|
|
273
|
+
expect(misroute!.count).toBeGreaterThanOrEqual(1);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ─── 5. Ambient Learning Radar ───────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
describe('Ambient learning radar (#208)', () => {
|
|
280
|
+
it('analyze with high confidence auto-captures', () => {
|
|
281
|
+
const result = learningRadar.analyze({
|
|
282
|
+
type: 'explicit_capture',
|
|
283
|
+
title: 'Always validate inputs',
|
|
284
|
+
description: 'User explicitly asked to remember this validation pattern.',
|
|
285
|
+
confidence: 0.95,
|
|
286
|
+
});
|
|
287
|
+
expect(result).not.toBeNull();
|
|
288
|
+
expect(result!.status).toBe('captured');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('analyze with medium confidence queues as pending', () => {
|
|
292
|
+
const result = learningRadar.analyze({
|
|
293
|
+
type: 'correction',
|
|
294
|
+
title: 'Use execFileSync not execSync',
|
|
295
|
+
description: 'User corrected shell execution to avoid injection.',
|
|
296
|
+
});
|
|
297
|
+
expect(result).not.toBeNull();
|
|
298
|
+
// Default correction confidence is 0.75 → pending
|
|
299
|
+
expect(result!.status).toBe('pending');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('analyze with low confidence logs only', () => {
|
|
303
|
+
const result = learningRadar.analyze({
|
|
304
|
+
type: 'pattern_success',
|
|
305
|
+
title: 'FTS5 search worked',
|
|
306
|
+
description: 'Vault search returned good results.',
|
|
307
|
+
confidence: 0.3,
|
|
308
|
+
});
|
|
309
|
+
expect(result).not.toBeNull();
|
|
310
|
+
expect(result!.status).toBe('logged');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('getCandidates returns pending candidates', () => {
|
|
314
|
+
const candidates = learningRadar.getCandidates();
|
|
315
|
+
expect(candidates.length).toBeGreaterThanOrEqual(1);
|
|
316
|
+
expect(candidates[0].status).toBe('pending');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('approve captures a pending candidate', () => {
|
|
320
|
+
const candidates = learningRadar.getCandidates();
|
|
321
|
+
const pending = candidates[0];
|
|
322
|
+
const result = learningRadar.approve(pending.id);
|
|
323
|
+
expect(result.captured).toBe(true);
|
|
324
|
+
expect(result.entryId).toBeDefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('dismiss marks candidate as dismissed', () => {
|
|
328
|
+
// Create another pending
|
|
329
|
+
learningRadar.analyze({
|
|
330
|
+
type: 'workaround',
|
|
331
|
+
title: 'Workaround for stale cache',
|
|
332
|
+
description: 'Clear cache before rebuilding.',
|
|
333
|
+
});
|
|
334
|
+
const candidates = learningRadar.getCandidates();
|
|
335
|
+
const pending = candidates.find((c) => c.title.includes('stale cache'));
|
|
336
|
+
expect(pending).toBeDefined();
|
|
337
|
+
const result = learningRadar.dismiss(pending!.id);
|
|
338
|
+
expect(result.dismissed).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('getStats returns radar statistics', () => {
|
|
342
|
+
const stats = learningRadar.getStats();
|
|
343
|
+
expect(stats.totalAnalyzed).toBeGreaterThanOrEqual(3);
|
|
344
|
+
expect(stats.autoCaptured).toBeGreaterThanOrEqual(1);
|
|
345
|
+
expect(stats.dismissed).toBeGreaterThanOrEqual(1);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('flush captures pending candidates above threshold', () => {
|
|
349
|
+
// Add a high-confidence pending
|
|
350
|
+
learningRadar.analyze({
|
|
351
|
+
type: 'search_miss',
|
|
352
|
+
title: 'No vault entry for caching patterns',
|
|
353
|
+
description: 'Repeated search for caching returned 0 results.',
|
|
354
|
+
confidence: 0.85,
|
|
355
|
+
});
|
|
356
|
+
const result = learningRadar.flush(0.8);
|
|
357
|
+
expect(result.captured).toBeGreaterThanOrEqual(0);
|
|
358
|
+
expect(Array.isArray(result.capturedIds)).toBe(true);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ─── 6. External Knowledge Ingestion ─────────────────────────────────
|
|
363
|
+
|
|
364
|
+
describe('External knowledge ingestion (#203)', () => {
|
|
365
|
+
it('ingestText returns graceful empty result without LLM', async () => {
|
|
366
|
+
const result = await textIngester.ingestText(
|
|
367
|
+
'This is a test article about software architecture patterns.',
|
|
368
|
+
{ type: 'article', title: 'Test Article' },
|
|
369
|
+
{ domain: 'architecture' },
|
|
370
|
+
);
|
|
371
|
+
// No LLM → 0 ingested (graceful degradation)
|
|
372
|
+
expect(result.ingested).toBe(0);
|
|
373
|
+
expect(result.source.title).toBe('Test Article');
|
|
374
|
+
expect(result.source.type).toBe('article');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('ingestUrl returns graceful empty result without LLM', async () => {
|
|
378
|
+
const result = await textIngester.ingestUrl('https://example.com');
|
|
379
|
+
expect(result.ingested).toBe(0);
|
|
380
|
+
expect(result.source.type).toBe('article');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('ingestBatch processes multiple items', async () => {
|
|
384
|
+
const results = await textIngester.ingestBatch([
|
|
385
|
+
{ text: 'Item 1', source: { type: 'notes', title: 'Note 1' } },
|
|
386
|
+
{ text: 'Item 2', source: { type: 'transcript', title: 'Talk 2' } },
|
|
387
|
+
]);
|
|
388
|
+
expect(results.length).toBe(2);
|
|
389
|
+
expect(results[0].source.title).toBe('Note 1');
|
|
390
|
+
expect(results[1].source.title).toBe('Talk 2');
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ─── 7. Content Synthesis ────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
describe('Content synthesis (#207)', () => {
|
|
397
|
+
it('synthesize returns raw entries when no LLM available', async () => {
|
|
398
|
+
const result = await synthesizer.synthesize('retry networking', {
|
|
399
|
+
format: 'brief',
|
|
400
|
+
maxEntries: 5,
|
|
401
|
+
});
|
|
402
|
+
expect(result.query).toBe('retry networking');
|
|
403
|
+
expect(result.format).toBe('brief');
|
|
404
|
+
expect(result.entriesConsulted).toBeGreaterThan(0);
|
|
405
|
+
expect(result.content).toContain('Retry');
|
|
406
|
+
expect(result.sources.length).toBeGreaterThan(0);
|
|
407
|
+
expect(result.coverage).toBeGreaterThanOrEqual(0);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('synthesize returns empty message for no results', async () => {
|
|
411
|
+
const result = await synthesizer.synthesize('quantum computing blockchain', {
|
|
412
|
+
format: 'outline',
|
|
413
|
+
});
|
|
414
|
+
expect(result.entriesConsulted).toBe(0);
|
|
415
|
+
expect(result.content).toContain('No vault entries found');
|
|
416
|
+
expect(result.gaps.length).toBeGreaterThanOrEqual(1);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('synthesize includes source attribution', async () => {
|
|
420
|
+
const result = await synthesizer.synthesize('architecture patterns', {
|
|
421
|
+
format: 'talking-points',
|
|
422
|
+
maxEntries: 3,
|
|
423
|
+
});
|
|
424
|
+
for (const source of result.sources) {
|
|
425
|
+
expect(source.id).toBeDefined();
|
|
426
|
+
expect(source.title).toBeDefined();
|
|
427
|
+
expect(source.score).toBeGreaterThanOrEqual(0);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('coverage score is between 0 and 100', async () => {
|
|
432
|
+
const result = await synthesizer.synthesize('testing tdd', {
|
|
433
|
+
format: 'post-draft',
|
|
434
|
+
});
|
|
435
|
+
expect(result.coverage).toBeGreaterThanOrEqual(0);
|
|
436
|
+
expect(result.coverage).toBeLessThanOrEqual(100);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ─── 8. Composable Skill Chains ──────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
describe('Composable skill chains (#204)', () => {
|
|
443
|
+
const mockDispatch = async (op: string, params: Record<string, unknown>): Promise<unknown> => {
|
|
444
|
+
// Simulate op results
|
|
445
|
+
if (op === 'search') return { results: [{ id: 'test', title: 'Test' }] };
|
|
446
|
+
if (op === 'capture_knowledge') return { captured: true, id: 'cap-1' };
|
|
447
|
+
return { ok: true, op, params };
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
it('executes a simple two-step chain', async () => {
|
|
451
|
+
const chain = {
|
|
452
|
+
id: 'test-chain',
|
|
453
|
+
name: 'Test Chain',
|
|
454
|
+
steps: [
|
|
455
|
+
{ id: 'step1', op: 'search', params: { query: 'retry' }, output: 'searchResult' },
|
|
456
|
+
{ id: 'step2', op: 'capture_knowledge', params: { title: 'From chain' } },
|
|
457
|
+
],
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const instance = await chainRunner.execute(chain, {}, mockDispatch);
|
|
461
|
+
expect(instance.status).toBe('completed');
|
|
462
|
+
expect(instance.stepsCompleted).toBe(2);
|
|
463
|
+
expect(instance.totalSteps).toBe(2);
|
|
464
|
+
expect(instance.stepOutputs.length).toBe(2);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('resolves $variable references between steps', async () => {
|
|
468
|
+
let capturedParams: Record<string, unknown> = {};
|
|
469
|
+
const trackingDispatch = async (op: string, params: Record<string, unknown>) => {
|
|
470
|
+
if (op === 'step2-op') capturedParams = params;
|
|
471
|
+
return { value: 'hello from step1' };
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const chain = {
|
|
475
|
+
id: 'var-chain',
|
|
476
|
+
steps: [
|
|
477
|
+
{ id: 'step1', op: 'step1-op', output: 'step1Result' },
|
|
478
|
+
{ id: 'step2', op: 'step2-op', params: { data: '$step1.value' } },
|
|
479
|
+
],
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
await chainRunner.execute(chain, {}, trackingDispatch);
|
|
483
|
+
expect(capturedParams.data).toBe('hello from step1');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('resolves $input references', async () => {
|
|
487
|
+
let capturedParams: Record<string, unknown> = {};
|
|
488
|
+
const trackingDispatch = async (op: string, params: Record<string, unknown>) => {
|
|
489
|
+
capturedParams = params;
|
|
490
|
+
return { ok: true };
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const chain = {
|
|
494
|
+
id: 'input-chain',
|
|
495
|
+
steps: [{ id: 'step1', op: 'my-op', params: { url: '$input.targetUrl' } }],
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
await chainRunner.execute(chain, { targetUrl: 'https://example.com' }, trackingDispatch);
|
|
499
|
+
expect(capturedParams.url).toBe('https://example.com');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('pauses on user-approval gate', async () => {
|
|
503
|
+
const chain = {
|
|
504
|
+
id: 'gate-chain',
|
|
505
|
+
steps: [
|
|
506
|
+
{ id: 'step1', op: 'search', gate: 'user-approval' as const },
|
|
507
|
+
{ id: 'step2', op: 'capture_knowledge' },
|
|
508
|
+
],
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const instance = await chainRunner.execute(chain, {}, mockDispatch);
|
|
512
|
+
expect(instance.status).toBe('paused');
|
|
513
|
+
expect(instance.pausedAtGate).toBe('step1');
|
|
514
|
+
expect(instance.stepsCompleted).toBe(1);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('resumes a paused chain after approval', async () => {
|
|
518
|
+
const chain = {
|
|
519
|
+
id: 'resume-chain',
|
|
520
|
+
steps: [
|
|
521
|
+
{ id: 'step1', op: 'search', gate: 'user-approval' as const },
|
|
522
|
+
{ id: 'step2', op: 'capture_knowledge' },
|
|
523
|
+
],
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const paused = await chainRunner.execute(chain, {}, mockDispatch);
|
|
527
|
+
expect(paused.status).toBe('paused');
|
|
528
|
+
|
|
529
|
+
const resumed = await chainRunner.approve(paused.id, chain, mockDispatch);
|
|
530
|
+
expect(resumed.status).toBe('completed');
|
|
531
|
+
expect(resumed.stepsCompleted).toBe(2);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('fails on auto-test gate when step returns error', async () => {
|
|
535
|
+
const failDispatch = async () => ({ error: 'build failed' });
|
|
536
|
+
|
|
537
|
+
const chain = {
|
|
538
|
+
id: 'fail-chain',
|
|
539
|
+
steps: [{ id: 'step1', op: 'build', gate: 'auto-test' as const }],
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const instance = await chainRunner.execute(chain, {}, failDispatch);
|
|
543
|
+
expect(instance.status).toBe('failed');
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('getInstance returns persisted chain state', async () => {
|
|
547
|
+
const chain = {
|
|
548
|
+
id: 'persist-chain',
|
|
549
|
+
steps: [{ id: 'step1', op: 'search' }],
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const instance = await chainRunner.execute(chain, { key: 'value' }, mockDispatch);
|
|
553
|
+
const loaded = chainRunner.getInstance(instance.id);
|
|
554
|
+
expect(loaded).not.toBeNull();
|
|
555
|
+
expect(loaded!.chainId).toBe('persist-chain');
|
|
556
|
+
expect(loaded!.status).toBe('completed');
|
|
557
|
+
expect(loaded!.input.key).toBe('value');
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('list returns all chain instances', () => {
|
|
561
|
+
const instances = chainRunner.list();
|
|
562
|
+
expect(instances.length).toBeGreaterThanOrEqual(1);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('handles step failure gracefully', async () => {
|
|
566
|
+
const throwDispatch = async () => {
|
|
567
|
+
throw new Error('connection refused');
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const chain = {
|
|
571
|
+
id: 'error-chain',
|
|
572
|
+
steps: [
|
|
573
|
+
{ id: 'step1', op: 'failing-op' },
|
|
574
|
+
{ id: 'step2', op: 'should-not-run' },
|
|
575
|
+
],
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const instance = await chainRunner.execute(chain, {}, throwDispatch);
|
|
579
|
+
expect(instance.status).toBe('failed');
|
|
580
|
+
expect(instance.stepsCompleted).toBe(0);
|
|
581
|
+
expect(instance.stepOutputs[0].status).toBe('failed');
|
|
582
|
+
});
|
|
583
|
+
});
|