@hawon/nexus 0.1.0 → 0.3.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/README.md +60 -38
- package/dist/cli/index.js +76 -145
- package/dist/index.js +15 -26
- package/dist/mcp/server.js +61 -32
- package/package.json +2 -1
- package/scripts/auto-skill.sh +54 -0
- package/scripts/auto-sync.sh +11 -0
- package/scripts/benchmark.ts +444 -0
- package/scripts/scan-tool-result.sh +46 -0
- package/src/cli/index.ts +79 -172
- package/src/index.ts +17 -29
- package/src/mcp/server.ts +67 -41
- package/src/memory-engine/index.ts +4 -6
- package/src/memory-engine/nexus-memory.test.ts +437 -0
- package/src/memory-engine/nexus-memory.ts +631 -0
- package/src/memory-engine/semantic.ts +380 -0
- package/src/parser/parse.ts +1 -21
- package/src/promptguard/advanced-rules.ts +129 -12
- package/src/promptguard/entropy.ts +21 -2
- package/src/promptguard/evolution/auto-update.ts +16 -6
- package/src/promptguard/multilingual-rules.ts +68 -0
- package/src/promptguard/rules.ts +87 -2
- package/src/promptguard/scanner.test.ts +262 -0
- package/src/promptguard/scanner.ts +1 -1
- package/src/promptguard/semantic.ts +19 -4
- package/src/promptguard/token-analysis.ts +17 -5
- package/src/review/analyzer.test.ts +279 -0
- package/src/review/analyzer.ts +112 -28
- package/src/shared/stop-words.ts +21 -0
- package/src/skills/index.ts +11 -27
- package/src/skills/memory-skill-engine.ts +1044 -0
- package/src/testing/health-check.ts +19 -2
- package/src/cost/index.ts +0 -3
- package/src/cost/tracker.ts +0 -290
- package/src/cost/types.ts +0 -34
- package/src/memory-engine/compressor.ts +0 -97
- package/src/memory-engine/context-window.ts +0 -113
- package/src/memory-engine/store.ts +0 -371
- package/src/memory-engine/types.ts +0 -32
- package/src/skills/context-engine.ts +0 -863
- package/src/skills/extractor.ts +0 -224
- package/src/skills/global-context.ts +0 -726
- package/src/skills/library.ts +0 -189
- package/src/skills/pattern-engine.ts +0 -712
- package/src/skills/render-evolved.ts +0 -160
- package/src/skills/skill-reconciler.ts +0 -703
- package/src/skills/smart-extractor.ts +0 -843
- package/src/skills/types.ts +0 -18
- package/src/skills/wisdom-extractor.ts +0 -737
- package/src/superdev-evolution/index.ts +0 -3
- package/src/superdev-evolution/skill-manager.ts +0 -266
- package/src/superdev-evolution/types.ts +0 -20
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
export type {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export type { ContextWindow, ContextWindowConfig } from "./context-window.js";
|
|
6
|
-
export { createContextWindow } from "./context-window.js";
|
|
1
|
+
export { createNexusMemory, extractObservations } from "./nexus-memory.js";
|
|
2
|
+
export type { NexusMemory, Observation, KnowledgeNode, KnowledgeEdge, MemoryResult, MemoryStats } from "./nexus-memory.js";
|
|
3
|
+
export { getSynonyms, expandQuery, semanticSimilarity, createCoOccurrenceModel } from "./semantic.js";
|
|
4
|
+
export type { CoOccurrenceModel, ExpandedQuery } from "./semantic.js";
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { createNexusMemory } from "./nexus-memory.js";
|
|
7
|
+
import { semanticSimilarity, expandQuery, getSynonyms } from "./semantic.js";
|
|
8
|
+
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
10
|
+
// Test helpers
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
12
|
+
|
|
13
|
+
function makeTmpDir(): string {
|
|
14
|
+
return mkdtempSync(join(tmpdir(), "nexus-test-"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SAMPLE_TEXT = `
|
|
18
|
+
The deploy pipeline uses Docker containers on AWS ECS.
|
|
19
|
+
We fixed a critical authentication bug in the login endpoint.
|
|
20
|
+
The React frontend communicates with the REST API via fetch.
|
|
21
|
+
Performance testing showed the cache layer reduces latency by 40%.
|
|
22
|
+
Database migrations should be run before deploying to production.
|
|
23
|
+
The security audit found an SQL injection vulnerability in the search endpoint.
|
|
24
|
+
Git branching strategy uses trunk-based development with feature flags.
|
|
25
|
+
The test coverage for the payment module is below 60%.
|
|
26
|
+
Server monitoring is handled by Prometheus and Grafana dashboards.
|
|
27
|
+
The TypeScript compiler catches most type errors at build time.
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const SAMPLE_TEXT_2 = `
|
|
31
|
+
The Kubernetes cluster autoscales based on CPU utilization.
|
|
32
|
+
Redis cache invalidation happens on every deploy event.
|
|
33
|
+
The authentication service uses JWT tokens with short expiry.
|
|
34
|
+
Error handling in the API uses a centralized middleware pattern.
|
|
35
|
+
The CI/CD pipeline runs unit tests before every merge to main.
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
39
|
+
// Ingest
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
describe("ingest", () => {
|
|
43
|
+
let dir: string;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
dir = makeTmpDir();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
rmSync(dir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("ingests text and creates observations", () => {
|
|
54
|
+
const mem = createNexusMemory(dir);
|
|
55
|
+
const count = mem.ingest(SAMPLE_TEXT, "nexus-project", "session-1");
|
|
56
|
+
assert.ok(count > 0, `Expected observations to be created, got ${count}`);
|
|
57
|
+
const stats = mem.getStats();
|
|
58
|
+
assert.equal(stats.totalObservations, count);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("deduplicates on repeated ingest", () => {
|
|
62
|
+
const mem = createNexusMemory(dir);
|
|
63
|
+
const first = mem.ingest(SAMPLE_TEXT, "nexus-project");
|
|
64
|
+
const second = mem.ingest(SAMPLE_TEXT, "nexus-project");
|
|
65
|
+
assert.equal(second, 0, "Duplicate ingest should add 0 new observations");
|
|
66
|
+
assert.equal(mem.getStats().totalObservations, first);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("ingests from multiple domains", () => {
|
|
70
|
+
const mem = createNexusMemory(dir);
|
|
71
|
+
mem.ingest(SAMPLE_TEXT, "project-a");
|
|
72
|
+
mem.ingest(SAMPLE_TEXT_2, "project-b");
|
|
73
|
+
const stats = mem.getStats();
|
|
74
|
+
assert.ok(stats.domains.includes("project-a"));
|
|
75
|
+
assert.ok(stats.domains.includes("project-b"));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
80
|
+
// L1 scan — metadata filtering
|
|
81
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
82
|
+
|
|
83
|
+
describe("L1 scanIndex", () => {
|
|
84
|
+
let dir: string;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
dir = makeTmpDir();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
rmSync(dir, { recursive: true, force: true });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("filters by domain", () => {
|
|
95
|
+
const mem = createNexusMemory(dir);
|
|
96
|
+
mem.ingest(SAMPLE_TEXT, "project-a");
|
|
97
|
+
mem.ingest(SAMPLE_TEXT_2, "project-b");
|
|
98
|
+
|
|
99
|
+
const resultsA = mem.scanIndex("project-a");
|
|
100
|
+
const resultsB = mem.scanIndex("project-b");
|
|
101
|
+
|
|
102
|
+
assert.ok(resultsA.length > 0);
|
|
103
|
+
assert.ok(resultsB.length > 0);
|
|
104
|
+
assert.ok(resultsA.every((o) => o.domain === "project-a"));
|
|
105
|
+
assert.ok(resultsB.every((o) => o.domain === "project-b"));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("filters by tags", () => {
|
|
109
|
+
const mem = createNexusMemory(dir);
|
|
110
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
111
|
+
|
|
112
|
+
const securityResults = mem.scanIndex(undefined, undefined, ["security"]);
|
|
113
|
+
if (securityResults.length > 0) {
|
|
114
|
+
assert.ok(securityResults.every((o) => o.tags.includes("security")));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("returns all valid observations when no filter applied", () => {
|
|
119
|
+
const mem = createNexusMemory(dir);
|
|
120
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
121
|
+
const all = mem.scanIndex();
|
|
122
|
+
const stats = mem.getStats();
|
|
123
|
+
assert.equal(all.length, stats.validObservations);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
128
|
+
// L2 search — BM25
|
|
129
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
130
|
+
|
|
131
|
+
describe("L2 search (BM25)", () => {
|
|
132
|
+
let dir: string;
|
|
133
|
+
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
dir = makeTmpDir();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
rmSync(dir, { recursive: true, force: true });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns relevant results for keyword query", () => {
|
|
143
|
+
const mem = createNexusMemory(dir);
|
|
144
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
145
|
+
|
|
146
|
+
const results = mem.search("deploy");
|
|
147
|
+
assert.ok(results.length > 0, "Expected BM25 results for 'deploy'");
|
|
148
|
+
assert.ok(results[0].score > 0, "Top result should have positive score");
|
|
149
|
+
assert.equal(results[0].retrievalLevel, "L2");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("semantic search: Korean query finds English results", () => {
|
|
153
|
+
const mem = createNexusMemory(dir);
|
|
154
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
155
|
+
|
|
156
|
+
// "배포" is a synonym for "deploy" in the semantic engine
|
|
157
|
+
const results = mem.search("배포");
|
|
158
|
+
assert.ok(results.length > 0, "Korean query '배포' should find deploy-related results");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns results sorted by score descending", () => {
|
|
162
|
+
const mem = createNexusMemory(dir);
|
|
163
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
164
|
+
|
|
165
|
+
const results = mem.search("security vulnerability");
|
|
166
|
+
if (results.length >= 2) {
|
|
167
|
+
for (let i = 1; i < results.length; i++) {
|
|
168
|
+
assert.ok(results[i - 1].score >= results[i].score, "Results should be sorted by score desc");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("respects limit parameter", () => {
|
|
174
|
+
const mem = createNexusMemory(dir);
|
|
175
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
176
|
+
|
|
177
|
+
const results = mem.search("deploy", 2);
|
|
178
|
+
assert.ok(results.length <= 2);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
183
|
+
// L3 deep search — graph expansion
|
|
184
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
185
|
+
|
|
186
|
+
describe("L3 deepSearch (graph expansion)", () => {
|
|
187
|
+
let dir: string;
|
|
188
|
+
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
dir = makeTmpDir();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
afterEach(() => {
|
|
194
|
+
rmSync(dir, { recursive: true, force: true });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("finds related content via graph expansion", () => {
|
|
198
|
+
const mem = createNexusMemory(dir);
|
|
199
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
200
|
+
mem.ingest(SAMPLE_TEXT_2, "nexus");
|
|
201
|
+
|
|
202
|
+
const results = mem.deepSearch("deploy");
|
|
203
|
+
assert.ok(results.length > 0, "Deep search should return results");
|
|
204
|
+
|
|
205
|
+
// Deep search should find at least as many or more diverse results
|
|
206
|
+
const l2Only = mem.search("deploy");
|
|
207
|
+
// L3 may include L2 results plus graph-expanded L3 results
|
|
208
|
+
const hasL3 = results.some((r) => r.retrievalLevel === "L3");
|
|
209
|
+
const hasL2 = results.some((r) => r.retrievalLevel === "L2");
|
|
210
|
+
assert.ok(hasL2, "Deep search should include L2 results");
|
|
211
|
+
// L3 results are only present if graph expansion finds related content
|
|
212
|
+
// This is data-dependent, so we just verify the search completes
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
217
|
+
// Confirm / Invalidate
|
|
218
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
219
|
+
|
|
220
|
+
describe("confirm / invalidate", () => {
|
|
221
|
+
let dir: string;
|
|
222
|
+
|
|
223
|
+
beforeEach(() => {
|
|
224
|
+
dir = makeTmpDir();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
rmSync(dir, { recursive: true, force: true });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("confirm increases confidence", () => {
|
|
232
|
+
const mem = createNexusMemory(dir);
|
|
233
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
234
|
+
|
|
235
|
+
const obs = mem.scanIndex()[0];
|
|
236
|
+
const oldConf = obs.confidence;
|
|
237
|
+
mem.confirm(obs.id);
|
|
238
|
+
assert.ok(obs.confidence > oldConf, "Confidence should increase after confirm");
|
|
239
|
+
assert.ok(obs.confidence <= 1, "Confidence should not exceed 1");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("invalidate sets valid=false and confidence=0", () => {
|
|
243
|
+
const mem = createNexusMemory(dir);
|
|
244
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
245
|
+
|
|
246
|
+
const obs = mem.scanIndex()[0];
|
|
247
|
+
mem.invalidate(obs.id);
|
|
248
|
+
assert.equal(obs.valid, false);
|
|
249
|
+
assert.equal(obs.confidence, 0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("invalidated observations are excluded from scanIndex", () => {
|
|
253
|
+
const mem = createNexusMemory(dir);
|
|
254
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
255
|
+
|
|
256
|
+
const allBefore = mem.scanIndex();
|
|
257
|
+
const target = allBefore[0];
|
|
258
|
+
mem.invalidate(target.id);
|
|
259
|
+
const allAfter = mem.scanIndex();
|
|
260
|
+
assert.equal(allAfter.length, allBefore.length - 1);
|
|
261
|
+
assert.ok(!allAfter.some((o) => o.id === target.id));
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
266
|
+
// Persistence (save/load)
|
|
267
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
268
|
+
|
|
269
|
+
describe("persistence", () => {
|
|
270
|
+
let dir: string;
|
|
271
|
+
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
dir = makeTmpDir();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
afterEach(() => {
|
|
277
|
+
rmSync(dir, { recursive: true, force: true });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("persists and reloads observations", () => {
|
|
281
|
+
const mem1 = createNexusMemory(dir);
|
|
282
|
+
mem1.ingest(SAMPLE_TEXT, "nexus");
|
|
283
|
+
const countBefore = mem1.getStats().totalObservations;
|
|
284
|
+
mem1.save();
|
|
285
|
+
|
|
286
|
+
// Create a new instance from same dir — should load saved data
|
|
287
|
+
const mem2 = createNexusMemory(dir);
|
|
288
|
+
assert.equal(mem2.getStats().totalObservations, countBefore);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
293
|
+
// Semantic module — getSynonyms
|
|
294
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
295
|
+
|
|
296
|
+
describe("getSynonyms", () => {
|
|
297
|
+
it("returns synonyms for 'deploy'", () => {
|
|
298
|
+
const syns = getSynonyms("deploy");
|
|
299
|
+
assert.ok(syns.length > 0, "Expected synonyms for 'deploy'");
|
|
300
|
+
assert.ok(syns.includes("배포"), "Expected '배포' in synonyms for 'deploy'");
|
|
301
|
+
assert.ok(syns.includes("release"), "Expected 'release' in synonyms for 'deploy'");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("returns synonyms for Korean term '에러'", () => {
|
|
305
|
+
const syns = getSynonyms("에러");
|
|
306
|
+
assert.ok(syns.includes("error"), "Expected 'error' in synonyms for '에러'");
|
|
307
|
+
assert.ok(syns.includes("bug"), "Expected 'bug' in synonyms for '에러'");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("returns empty array for unknown word", () => {
|
|
311
|
+
const syns = getSynonyms("xyzzy12345");
|
|
312
|
+
assert.equal(syns.length, 0);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
317
|
+
// Semantic module — expandQuery
|
|
318
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
319
|
+
|
|
320
|
+
describe("expandQuery", () => {
|
|
321
|
+
it("expands '배포 에러' to include English equivalents", () => {
|
|
322
|
+
const result = expandQuery("배포 에러");
|
|
323
|
+
assert.ok(result.original.length > 0);
|
|
324
|
+
assert.ok(result.expanded.length > result.original.length, "Expanded should be larger than original");
|
|
325
|
+
|
|
326
|
+
const expandedSet = new Set(result.expanded);
|
|
327
|
+
// Should include English synonyms
|
|
328
|
+
assert.ok(
|
|
329
|
+
expandedSet.has("deploy") || expandedSet.has("release") || expandedSet.has("ship"),
|
|
330
|
+
"Expected English deploy synonym in expansion",
|
|
331
|
+
);
|
|
332
|
+
assert.ok(
|
|
333
|
+
expandedSet.has("error") || expandedSet.has("bug") || expandedSet.has("issue"),
|
|
334
|
+
"Expected English error synonym in expansion",
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("tracks expansion sources", () => {
|
|
339
|
+
const result = expandQuery("deploy");
|
|
340
|
+
assert.ok(result.expansions.length > 0, "Expected expansion entries");
|
|
341
|
+
assert.ok(result.expansions.every((e) => e.source === "synonym" || e.source === "cooccurrence"));
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
346
|
+
// Semantic module — semanticSimilarity
|
|
347
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
348
|
+
|
|
349
|
+
describe("semanticSimilarity", () => {
|
|
350
|
+
it("'deploy error' and '배포 에러' have similarity > 0.3", () => {
|
|
351
|
+
const score = semanticSimilarity("deploy error", "배포 에러");
|
|
352
|
+
assert.ok(score > 0.3, `Expected similarity > 0.3, got ${score}`);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("unrelated terms have similarity = 0", () => {
|
|
356
|
+
const score = semanticSimilarity("react", "서버");
|
|
357
|
+
// react and 서버 are in different synonym groups, no overlap
|
|
358
|
+
assert.equal(score, 0, `Expected 0 similarity for unrelated terms, got ${score}`);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("identical texts have high similarity", () => {
|
|
362
|
+
const score = semanticSimilarity("deploy to production", "deploy to production");
|
|
363
|
+
assert.ok(score > 0.5, `Expected high similarity for identical text, got ${score}`);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("returns 0 for empty input", () => {
|
|
367
|
+
assert.equal(semanticSimilarity("", "hello"), 0);
|
|
368
|
+
assert.equal(semanticSimilarity("hello", ""), 0);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
373
|
+
// Knowledge graph
|
|
374
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
375
|
+
|
|
376
|
+
describe("knowledge graph", () => {
|
|
377
|
+
let dir: string;
|
|
378
|
+
|
|
379
|
+
beforeEach(() => {
|
|
380
|
+
dir = makeTmpDir();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
afterEach(() => {
|
|
384
|
+
rmSync(dir, { recursive: true, force: true });
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("builds graph nodes and edges from observations", () => {
|
|
388
|
+
const mem = createNexusMemory(dir);
|
|
389
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
390
|
+
const graph = mem.getGraph();
|
|
391
|
+
assert.ok(graph.nodes.length > 0, "Expected graph nodes");
|
|
392
|
+
assert.ok(graph.edges.length > 0, "Expected graph edges");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("creates tunnels for shared topics across domains", () => {
|
|
396
|
+
const mem = createNexusMemory(dir);
|
|
397
|
+
mem.ingest(SAMPLE_TEXT, "project-a");
|
|
398
|
+
mem.ingest(SAMPLE_TEXT_2, "project-b");
|
|
399
|
+
const tunnels = mem.getTunnels();
|
|
400
|
+
// Tunnels connect domains that share topics
|
|
401
|
+
// Both texts discuss deploy/security/auth concepts
|
|
402
|
+
// Tunnel creation depends on topic extraction matching
|
|
403
|
+
assert.ok(Array.isArray(tunnels));
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
408
|
+
// Stats
|
|
409
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
410
|
+
|
|
411
|
+
describe("getStats", () => {
|
|
412
|
+
let dir: string;
|
|
413
|
+
|
|
414
|
+
beforeEach(() => {
|
|
415
|
+
dir = makeTmpDir();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
afterEach(() => {
|
|
419
|
+
rmSync(dir, { recursive: true, force: true });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("returns correct structure", () => {
|
|
423
|
+
const mem = createNexusMemory(dir);
|
|
424
|
+
mem.ingest(SAMPLE_TEXT, "nexus");
|
|
425
|
+
const stats = mem.getStats();
|
|
426
|
+
|
|
427
|
+
assert.ok(typeof stats.totalObservations === "number");
|
|
428
|
+
assert.ok(typeof stats.validObservations === "number");
|
|
429
|
+
assert.ok(typeof stats.graphNodes === "number");
|
|
430
|
+
assert.ok(typeof stats.graphEdges === "number");
|
|
431
|
+
assert.ok(typeof stats.tunnels === "number");
|
|
432
|
+
assert.ok(Array.isArray(stats.domains));
|
|
433
|
+
assert.ok(typeof stats.avgConfidence === "number");
|
|
434
|
+
assert.ok(typeof stats.avgDocLength === "number");
|
|
435
|
+
assert.ok(stats.avgConfidence >= 0 && stats.avgConfidence <= 1);
|
|
436
|
+
});
|
|
437
|
+
});
|