@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.
Files changed (52) hide show
  1. package/README.md +60 -38
  2. package/dist/cli/index.js +76 -145
  3. package/dist/index.js +15 -26
  4. package/dist/mcp/server.js +61 -32
  5. package/package.json +2 -1
  6. package/scripts/auto-skill.sh +54 -0
  7. package/scripts/auto-sync.sh +11 -0
  8. package/scripts/benchmark.ts +444 -0
  9. package/scripts/scan-tool-result.sh +46 -0
  10. package/src/cli/index.ts +79 -172
  11. package/src/index.ts +17 -29
  12. package/src/mcp/server.ts +67 -41
  13. package/src/memory-engine/index.ts +4 -6
  14. package/src/memory-engine/nexus-memory.test.ts +437 -0
  15. package/src/memory-engine/nexus-memory.ts +631 -0
  16. package/src/memory-engine/semantic.ts +380 -0
  17. package/src/parser/parse.ts +1 -21
  18. package/src/promptguard/advanced-rules.ts +129 -12
  19. package/src/promptguard/entropy.ts +21 -2
  20. package/src/promptguard/evolution/auto-update.ts +16 -6
  21. package/src/promptguard/multilingual-rules.ts +68 -0
  22. package/src/promptguard/rules.ts +87 -2
  23. package/src/promptguard/scanner.test.ts +262 -0
  24. package/src/promptguard/scanner.ts +1 -1
  25. package/src/promptguard/semantic.ts +19 -4
  26. package/src/promptguard/token-analysis.ts +17 -5
  27. package/src/review/analyzer.test.ts +279 -0
  28. package/src/review/analyzer.ts +112 -28
  29. package/src/shared/stop-words.ts +21 -0
  30. package/src/skills/index.ts +11 -27
  31. package/src/skills/memory-skill-engine.ts +1044 -0
  32. package/src/testing/health-check.ts +19 -2
  33. package/src/cost/index.ts +0 -3
  34. package/src/cost/tracker.ts +0 -290
  35. package/src/cost/types.ts +0 -34
  36. package/src/memory-engine/compressor.ts +0 -97
  37. package/src/memory-engine/context-window.ts +0 -113
  38. package/src/memory-engine/store.ts +0 -371
  39. package/src/memory-engine/types.ts +0 -32
  40. package/src/skills/context-engine.ts +0 -863
  41. package/src/skills/extractor.ts +0 -224
  42. package/src/skills/global-context.ts +0 -726
  43. package/src/skills/library.ts +0 -189
  44. package/src/skills/pattern-engine.ts +0 -712
  45. package/src/skills/render-evolved.ts +0 -160
  46. package/src/skills/skill-reconciler.ts +0 -703
  47. package/src/skills/smart-extractor.ts +0 -843
  48. package/src/skills/types.ts +0 -18
  49. package/src/skills/wisdom-extractor.ts +0 -737
  50. package/src/superdev-evolution/index.ts +0 -3
  51. package/src/superdev-evolution/skill-manager.ts +0 -266
  52. package/src/superdev-evolution/types.ts +0 -20
@@ -1,6 +1,4 @@
1
- export type { MemoryTier, MemoryEntry, MemoryQuery, MemoryStats } from "./types.js";
2
- export type { MemoryStore } from "./store.js";
3
- export { createMemoryStore } from "./store.js";
4
- export { compressEntry, extractKeyLines, estimateSizeBytes } from "./compressor.js";
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
+ });