dpth 0.2.0 → 0.4.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 +101 -148
- package/dist/adapter-sqlite.d.ts +17 -2
- package/dist/adapter-sqlite.d.ts.map +1 -1
- package/dist/adapter-sqlite.js +127 -16
- package/dist/adapter-sqlite.js.map +1 -1
- package/dist/dpth.d.ts +211 -0
- package/dist/dpth.d.ts.map +1 -0
- package/dist/dpth.js +665 -0
- package/dist/dpth.js.map +1 -0
- package/dist/entity.d.ts +1 -0
- package/dist/entity.d.ts.map +1 -1
- package/dist/entity.js +21 -5
- package/dist/entity.js.map +1 -1
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +63 -0
- package/dist/errors.js.map +1 -0
- package/dist/experimental/agent-sdk.d.ts +157 -0
- package/dist/experimental/agent-sdk.d.ts.map +1 -0
- package/dist/experimental/agent-sdk.js +367 -0
- package/dist/experimental/agent-sdk.js.map +1 -0
- package/dist/experimental/economics.d.ts +203 -0
- package/dist/experimental/economics.d.ts.map +1 -0
- package/dist/experimental/economics.js +510 -0
- package/dist/experimental/economics.js.map +1 -0
- package/dist/experimental/fallback.d.ts +104 -0
- package/dist/experimental/fallback.d.ts.map +1 -0
- package/dist/experimental/fallback.js +359 -0
- package/dist/experimental/fallback.js.map +1 -0
- package/dist/experimental/federation.d.ts +224 -0
- package/dist/experimental/federation.d.ts.map +1 -0
- package/dist/experimental/federation.js +377 -0
- package/dist/experimental/federation.js.map +1 -0
- package/dist/experimental/index.d.ts +20 -0
- package/dist/experimental/index.d.ts.map +1 -0
- package/dist/experimental/index.js +20 -0
- package/dist/experimental/index.js.map +1 -0
- package/dist/index.d.ts +14 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -14
- package/dist/index.js.map +1 -1
- package/dist/storage.d.ts +10 -11
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +4 -11
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -1
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +20 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +28 -0
- package/dist/util.js.map +1 -0
- package/package.json +18 -26
package/dist/dpth.js
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dpth.io — Unified Database API
|
|
3
|
+
*
|
|
4
|
+
* The main entry point. One line to get a persistent, intelligent database
|
|
5
|
+
* with entity resolution, temporal history, correlation detection, and
|
|
6
|
+
* optional vector search.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { dpth } from 'dpth/dpth';
|
|
10
|
+
* const db = await dpth('./myapp.db');
|
|
11
|
+
*
|
|
12
|
+
* // Entity resolution
|
|
13
|
+
* const john = db.entity.resolve('person', 'John Smith', 'stripe', 'cus_123');
|
|
14
|
+
* db.entity.resolve('person', 'jsmith', 'github', 'jsmith'); // auto-merged
|
|
15
|
+
*
|
|
16
|
+
* // Temporal history
|
|
17
|
+
* db.temporal.snapshot('dashboard', { revenue: 50000, users: 200 });
|
|
18
|
+
* db.temporal.history('dashboard'); // all snapshots over time
|
|
19
|
+
*
|
|
20
|
+
* // Correlation
|
|
21
|
+
* db.correlation.track('mrr', 50000);
|
|
22
|
+
* db.correlation.track('deploys', 12);
|
|
23
|
+
* db.correlation.find('mrr'); // what correlates with MRR?
|
|
24
|
+
*
|
|
25
|
+
* // Clean up
|
|
26
|
+
* await db.close();
|
|
27
|
+
*/
|
|
28
|
+
import { MemoryAdapter } from './storage.js';
|
|
29
|
+
import { ValidationError, AdapterCapabilityError } from './errors.js';
|
|
30
|
+
import { generateEntityId, generateSnapshotId } from './util.js';
|
|
31
|
+
// ─── Dpth Class ──────────────────────────────────────
|
|
32
|
+
export class Dpth {
|
|
33
|
+
adapter;
|
|
34
|
+
_ready;
|
|
35
|
+
/** Entity resolution and management */
|
|
36
|
+
entity;
|
|
37
|
+
/** Temporal history and snapshots */
|
|
38
|
+
temporal;
|
|
39
|
+
/** Correlation detection across metrics */
|
|
40
|
+
correlation;
|
|
41
|
+
/** Vector search (if adapter supports it) */
|
|
42
|
+
vector;
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.adapter = options.adapter || new MemoryAdapter();
|
|
45
|
+
this.entity = new EntityAPI(this.adapter);
|
|
46
|
+
this.temporal = new TemporalAPI(this.adapter);
|
|
47
|
+
this.correlation = new CorrelationAPI(this.adapter);
|
|
48
|
+
this.vector = new VectorAPI(this.adapter);
|
|
49
|
+
this._ready = this.init(options);
|
|
50
|
+
}
|
|
51
|
+
async init(options) {
|
|
52
|
+
// If path provided, dynamically load SQLite adapter
|
|
53
|
+
if (options.path) {
|
|
54
|
+
try {
|
|
55
|
+
const { SQLiteAdapter } = await import('./adapter-sqlite.js');
|
|
56
|
+
this.adapter = new SQLiteAdapter(options.path);
|
|
57
|
+
// Re-initialize APIs with the real adapter
|
|
58
|
+
this.entity = new EntityAPI(this.adapter);
|
|
59
|
+
this.temporal = new TemporalAPI(this.adapter);
|
|
60
|
+
this.correlation = new CorrelationAPI(this.adapter);
|
|
61
|
+
this.vector = new VectorAPI(this.adapter);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// SQLite not available, fall back to memory
|
|
65
|
+
console.warn('dpth: better-sqlite3 not installed, using in-memory storage');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Wait for initialization to complete */
|
|
70
|
+
async ready() {
|
|
71
|
+
await this._ready;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
/** Close the database and flush any pending writes */
|
|
75
|
+
async close() {
|
|
76
|
+
await this._ready;
|
|
77
|
+
await this.adapter.close();
|
|
78
|
+
}
|
|
79
|
+
/** Get database stats */
|
|
80
|
+
async stats() {
|
|
81
|
+
await this._ready;
|
|
82
|
+
return {
|
|
83
|
+
entities: await this.adapter.count('entities'),
|
|
84
|
+
snapshots: await this.adapter.count('snapshots'),
|
|
85
|
+
metrics: await this.adapter.count('metrics'),
|
|
86
|
+
vectors: await this.adapter.count('vectors'),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ─── Entity API ──────────────────────────────────────
|
|
91
|
+
class EntityAPI {
|
|
92
|
+
adapter;
|
|
93
|
+
constructor(adapter) {
|
|
94
|
+
this.adapter = adapter;
|
|
95
|
+
}
|
|
96
|
+
async resolve(typeOrOpts, name, sourceId, externalId, options) {
|
|
97
|
+
// Normalize to object form
|
|
98
|
+
let type;
|
|
99
|
+
let resolvedName;
|
|
100
|
+
let resolvedSourceId;
|
|
101
|
+
let resolvedExternalId;
|
|
102
|
+
let email;
|
|
103
|
+
let aliases;
|
|
104
|
+
let attributes;
|
|
105
|
+
let minConfidence;
|
|
106
|
+
if (typeof typeOrOpts === 'object') {
|
|
107
|
+
// Object form (preferred)
|
|
108
|
+
if (!typeOrOpts.type)
|
|
109
|
+
throw new ValidationError('resolve() requires a non-empty "type" (e.g. "person", "company")');
|
|
110
|
+
if (!typeOrOpts.name)
|
|
111
|
+
throw new ValidationError('resolve() requires a non-empty "name"');
|
|
112
|
+
if (!typeOrOpts.source)
|
|
113
|
+
throw new ValidationError('resolve() requires a non-empty "source" (e.g. "stripe", "github")');
|
|
114
|
+
if (!typeOrOpts.externalId)
|
|
115
|
+
throw new ValidationError('resolve() requires a non-empty "externalId"');
|
|
116
|
+
type = typeOrOpts.type;
|
|
117
|
+
resolvedName = typeOrOpts.name;
|
|
118
|
+
resolvedSourceId = typeOrOpts.source;
|
|
119
|
+
resolvedExternalId = typeOrOpts.externalId;
|
|
120
|
+
email = typeOrOpts.email;
|
|
121
|
+
aliases = typeOrOpts.aliases;
|
|
122
|
+
attributes = typeOrOpts.attributes;
|
|
123
|
+
minConfidence = typeOrOpts.minConfidence ?? 0.7;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Legacy positional form
|
|
127
|
+
type = typeOrOpts;
|
|
128
|
+
resolvedName = name;
|
|
129
|
+
resolvedSourceId = sourceId;
|
|
130
|
+
resolvedExternalId = externalId;
|
|
131
|
+
email = options?.email;
|
|
132
|
+
aliases = options?.aliases;
|
|
133
|
+
attributes = options?.attributes;
|
|
134
|
+
minConfidence = options?.minConfidence ?? 0.7;
|
|
135
|
+
}
|
|
136
|
+
const sKey = `${resolvedSourceId}:${resolvedExternalId}`;
|
|
137
|
+
// Check source index first (exact source match)
|
|
138
|
+
const existingId = await this.adapter.get('source_index', sKey);
|
|
139
|
+
if (existingId) {
|
|
140
|
+
const entity = await this.adapter.get('entities', existingId);
|
|
141
|
+
if (entity) {
|
|
142
|
+
// Update last seen
|
|
143
|
+
const ref = entity.sources.find(s => s.sourceId === resolvedSourceId && s.externalId === resolvedExternalId);
|
|
144
|
+
if (ref)
|
|
145
|
+
ref.lastSeen = new Date();
|
|
146
|
+
await this.adapter.put('entities', entity.id, entity);
|
|
147
|
+
return { entity, isNew: false, confidence: 1.0 };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Try fuzzy matching against existing entities
|
|
151
|
+
const match = await this.findBestMatch(type, resolvedName, email, aliases);
|
|
152
|
+
if (match && match.score >= minConfidence) {
|
|
153
|
+
const entity = match.entity;
|
|
154
|
+
// Merge: add source ref
|
|
155
|
+
entity.sources.push({
|
|
156
|
+
sourceId: resolvedSourceId,
|
|
157
|
+
externalId: resolvedExternalId,
|
|
158
|
+
confidence: match.score,
|
|
159
|
+
lastSeen: new Date(),
|
|
160
|
+
});
|
|
161
|
+
// Add alias
|
|
162
|
+
if (!entity.aliases.includes(resolvedName) && entity.name !== resolvedName) {
|
|
163
|
+
entity.aliases.push(resolvedName);
|
|
164
|
+
}
|
|
165
|
+
// Merge attributes
|
|
166
|
+
if (attributes) {
|
|
167
|
+
const now = new Date();
|
|
168
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
169
|
+
const existing = entity.attributes[key];
|
|
170
|
+
if (existing) {
|
|
171
|
+
const current = existing.history.find(h => h.validTo === null);
|
|
172
|
+
if (current)
|
|
173
|
+
current.validTo = now;
|
|
174
|
+
existing.history.push({ value, validFrom: now, validTo: null, source: resolvedSourceId });
|
|
175
|
+
existing.current = value;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
entity.attributes[key] = {
|
|
179
|
+
current: value,
|
|
180
|
+
history: [{ value, validFrom: now, validTo: null, source: resolvedSourceId }],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
entity.updatedAt = new Date();
|
|
186
|
+
await this.adapter.put('entities', entity.id, entity);
|
|
187
|
+
await this.adapter.put('source_index', sKey, entity.id);
|
|
188
|
+
await this.updateEmailIndex(entity);
|
|
189
|
+
return { entity, isNew: false, confidence: match.score };
|
|
190
|
+
}
|
|
191
|
+
// Create new entity
|
|
192
|
+
const id = generateEntityId();
|
|
193
|
+
const now = new Date();
|
|
194
|
+
const entityAttrs = {};
|
|
195
|
+
if (attributes) {
|
|
196
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
197
|
+
entityAttrs[key] = {
|
|
198
|
+
current: value,
|
|
199
|
+
history: [{ value, validFrom: now, validTo: null, source: resolvedSourceId }],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (email) {
|
|
204
|
+
entityAttrs['email'] = {
|
|
205
|
+
current: email,
|
|
206
|
+
history: [{ value: email, validFrom: now, validTo: null, source: resolvedSourceId }],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const entity = {
|
|
210
|
+
id,
|
|
211
|
+
type,
|
|
212
|
+
name: resolvedName,
|
|
213
|
+
aliases: aliases || [],
|
|
214
|
+
sources: [{ sourceId: resolvedSourceId, externalId: resolvedExternalId, confidence: 1.0, lastSeen: now }],
|
|
215
|
+
attributes: entityAttrs,
|
|
216
|
+
createdAt: now,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
};
|
|
219
|
+
await this.adapter.put('entities', id, entity);
|
|
220
|
+
await this.adapter.put('source_index', sKey, id);
|
|
221
|
+
await this.updateEmailIndex(entity);
|
|
222
|
+
return { entity, isNew: true, confidence: 1.0 };
|
|
223
|
+
}
|
|
224
|
+
/** Get entity by ID */
|
|
225
|
+
async get(id) {
|
|
226
|
+
return await this.adapter.get('entities', id);
|
|
227
|
+
}
|
|
228
|
+
/** Find entity by source reference */
|
|
229
|
+
async findBySource(sourceId, externalId) {
|
|
230
|
+
const id = await this.adapter.get('source_index', `${sourceId}:${externalId}`);
|
|
231
|
+
return id ? await this.get(id) : undefined;
|
|
232
|
+
}
|
|
233
|
+
/** Get all entities of a type */
|
|
234
|
+
async list(type) {
|
|
235
|
+
const all = await this.adapter.query({
|
|
236
|
+
collection: 'entities',
|
|
237
|
+
...(type ? { where: { type } } : {}),
|
|
238
|
+
});
|
|
239
|
+
return all;
|
|
240
|
+
}
|
|
241
|
+
/** Update an entity attribute (temporal) */
|
|
242
|
+
async setAttribute(entityId, key, value, sourceId) {
|
|
243
|
+
const entity = await this.get(entityId);
|
|
244
|
+
if (!entity)
|
|
245
|
+
return undefined;
|
|
246
|
+
const now = new Date();
|
|
247
|
+
const existing = entity.attributes[key];
|
|
248
|
+
if (existing) {
|
|
249
|
+
const current = existing.history.find(h => h.validTo === null);
|
|
250
|
+
if (current)
|
|
251
|
+
current.validTo = now;
|
|
252
|
+
existing.history.push({ value, validFrom: now, validTo: null, source: sourceId });
|
|
253
|
+
existing.current = value;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
entity.attributes[key] = {
|
|
257
|
+
current: value,
|
|
258
|
+
history: [{ value, validFrom: now, validTo: null, source: sourceId }],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
entity.updatedAt = now;
|
|
262
|
+
await this.adapter.put('entities', entityId, entity);
|
|
263
|
+
// Update email index if email attribute changed
|
|
264
|
+
if (key === 'email') {
|
|
265
|
+
await this.updateEmailIndex(entity);
|
|
266
|
+
}
|
|
267
|
+
return entity;
|
|
268
|
+
}
|
|
269
|
+
/** Merge two entities */
|
|
270
|
+
async merge(keepId, mergeId) {
|
|
271
|
+
const keep = await this.get(keepId);
|
|
272
|
+
const merge = await this.get(mergeId);
|
|
273
|
+
if (!keep || !merge)
|
|
274
|
+
return undefined;
|
|
275
|
+
// Merge sources
|
|
276
|
+
for (const source of merge.sources) {
|
|
277
|
+
if (!keep.sources.find(s => s.sourceId === source.sourceId && s.externalId === source.externalId)) {
|
|
278
|
+
keep.sources.push(source);
|
|
279
|
+
}
|
|
280
|
+
await this.adapter.put('source_index', `${source.sourceId}:${source.externalId}`, keepId);
|
|
281
|
+
}
|
|
282
|
+
// Merge aliases
|
|
283
|
+
for (const alias of [...merge.aliases, merge.name]) {
|
|
284
|
+
if (!keep.aliases.includes(alias) && keep.name !== alias) {
|
|
285
|
+
keep.aliases.push(alias);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Merge attributes
|
|
289
|
+
for (const [key, value] of Object.entries(merge.attributes)) {
|
|
290
|
+
if (!keep.attributes[key]) {
|
|
291
|
+
keep.attributes[key] = value;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
keep.attributes[key].history.push(...value.history);
|
|
295
|
+
keep.attributes[key].history.sort((a, b) => new Date(a.validFrom).getTime() - new Date(b.validFrom).getTime());
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
keep.updatedAt = new Date();
|
|
299
|
+
await this.adapter.put('entities', keepId, keep);
|
|
300
|
+
await this.adapter.delete('entities', mergeId);
|
|
301
|
+
return keep;
|
|
302
|
+
}
|
|
303
|
+
/** Count entities */
|
|
304
|
+
async count(type) {
|
|
305
|
+
if (!type)
|
|
306
|
+
return this.adapter.count('entities');
|
|
307
|
+
const all = await this.list(type);
|
|
308
|
+
return all.length;
|
|
309
|
+
}
|
|
310
|
+
/** Update the email index for an entity */
|
|
311
|
+
async updateEmailIndex(entity) {
|
|
312
|
+
const email = entity.attributes['email']?.current;
|
|
313
|
+
if (email) {
|
|
314
|
+
await this.adapter.put('email_index', email.toLowerCase(), entity.id);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// ── Private matching ──
|
|
318
|
+
async findBestMatch(type, name, email, aliases) {
|
|
319
|
+
// ── Fast path: email index lookup (O(1) instead of O(n)) ──
|
|
320
|
+
if (email) {
|
|
321
|
+
const emailKey = email.toLowerCase();
|
|
322
|
+
const entityId = await this.adapter.get('email_index', emailKey);
|
|
323
|
+
if (entityId) {
|
|
324
|
+
const entity = await this.adapter.get('entities', entityId);
|
|
325
|
+
if (entity && entity.type === type) {
|
|
326
|
+
return { entity, score: 0.9 + (entity.name.toLowerCase() === name.toLowerCase() ? 0.1 : 0) };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// ── Blocking: narrow candidates before fuzzy matching ──
|
|
331
|
+
// Instead of scanning ALL entities, use name-based blocking
|
|
332
|
+
const candidates = await this.adapter.query({
|
|
333
|
+
collection: 'entities',
|
|
334
|
+
where: { type },
|
|
335
|
+
});
|
|
336
|
+
// For small sets (<500), just scan all — Levenshtein is fast enough
|
|
337
|
+
// For larger sets, use first-letter + length blocking to narrow
|
|
338
|
+
let narrowed;
|
|
339
|
+
if (candidates.length < 500) {
|
|
340
|
+
narrowed = candidates;
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const nameLower = name.toLowerCase();
|
|
344
|
+
const nameLen = nameLower.length;
|
|
345
|
+
narrowed = candidates.filter(e => {
|
|
346
|
+
const eName = e.name.toLowerCase();
|
|
347
|
+
// Block 1: first letter match OR alias match
|
|
348
|
+
if (eName[0] !== nameLower[0] &&
|
|
349
|
+
!e.aliases.some(a => a.toLowerCase()[0] === nameLower[0])) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
// Block 2: length within 50% (no point fuzzy-matching "Al" against "Alexander Hamilton")
|
|
353
|
+
if (Math.abs(eName.length - nameLen) > nameLen * 0.5) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
// Also include any entities that share email domain if we have email
|
|
359
|
+
if (email) {
|
|
360
|
+
const domain = email.split('@')[1]?.toLowerCase();
|
|
361
|
+
if (domain) {
|
|
362
|
+
for (const entity of candidates) {
|
|
363
|
+
const eEmail = entity.attributes['email']?.current;
|
|
364
|
+
if (eEmail?.toLowerCase().endsWith(`@${domain}`) && !narrowed.includes(entity)) {
|
|
365
|
+
narrowed.push(entity);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
let best = null;
|
|
372
|
+
const searchTerms = [name.toLowerCase(), ...(aliases || []).map(a => a.toLowerCase())];
|
|
373
|
+
if (email)
|
|
374
|
+
searchTerms.push(email.toLowerCase());
|
|
375
|
+
for (const entity of narrowed) {
|
|
376
|
+
let score = 0;
|
|
377
|
+
// Name matching
|
|
378
|
+
const entityName = entity.name.toLowerCase();
|
|
379
|
+
if (entityName === name.toLowerCase()) {
|
|
380
|
+
score += 0.8;
|
|
381
|
+
}
|
|
382
|
+
else if (this.fuzzyScore(entityName, name.toLowerCase()) > 0.85) {
|
|
383
|
+
score += 0.5;
|
|
384
|
+
}
|
|
385
|
+
else if (this.fuzzyScore(entityName, name.toLowerCase()) > 0.7) {
|
|
386
|
+
score += 0.3;
|
|
387
|
+
}
|
|
388
|
+
// Email matching
|
|
389
|
+
const entityEmail = entity.attributes['email']?.current;
|
|
390
|
+
if (email && entityEmail && entityEmail.toLowerCase() === email.toLowerCase()) {
|
|
391
|
+
score += 0.9;
|
|
392
|
+
}
|
|
393
|
+
// Alias matching
|
|
394
|
+
for (const alias of entity.aliases) {
|
|
395
|
+
if (searchTerms.includes(alias.toLowerCase())) {
|
|
396
|
+
score += 0.3;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
score = Math.min(1, score);
|
|
401
|
+
if (score > 0.3 && (!best || score > best.score)) {
|
|
402
|
+
best = { entity, score };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return best;
|
|
406
|
+
}
|
|
407
|
+
fuzzyScore(a, b) {
|
|
408
|
+
if (a === b)
|
|
409
|
+
return 1;
|
|
410
|
+
if (!a.length || !b.length)
|
|
411
|
+
return 0;
|
|
412
|
+
const matrix = [];
|
|
413
|
+
for (let i = 0; i <= a.length; i++)
|
|
414
|
+
matrix[i] = [i];
|
|
415
|
+
for (let j = 0; j <= b.length; j++)
|
|
416
|
+
matrix[0][j] = j;
|
|
417
|
+
for (let i = 1; i <= a.length; i++) {
|
|
418
|
+
for (let j = 1; j <= b.length; j++) {
|
|
419
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
420
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return 1 - matrix[a.length][b.length] / Math.max(a.length, b.length);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// ─── Temporal API ────────────────────────────────────
|
|
427
|
+
class TemporalAPI {
|
|
428
|
+
adapter;
|
|
429
|
+
constructor(adapter) {
|
|
430
|
+
this.adapter = adapter;
|
|
431
|
+
}
|
|
432
|
+
/** Take a snapshot of any data */
|
|
433
|
+
async snapshot(key, data, source = 'local') {
|
|
434
|
+
const record = {
|
|
435
|
+
id: generateSnapshotId(),
|
|
436
|
+
key,
|
|
437
|
+
timestamp: new Date(),
|
|
438
|
+
data,
|
|
439
|
+
source,
|
|
440
|
+
};
|
|
441
|
+
// Store snapshot directly — no separate index needed.
|
|
442
|
+
// history() uses query({ where: { key } }) to find all snapshots for a key.
|
|
443
|
+
// This avoids the old approach of maintaining a JSON array index that grew unbounded.
|
|
444
|
+
await this.adapter.put('snapshots', record.id, record);
|
|
445
|
+
return record;
|
|
446
|
+
}
|
|
447
|
+
/** Get all snapshots for a key (ordered by time) */
|
|
448
|
+
async history(key, options) {
|
|
449
|
+
const records = await this.adapter.query({
|
|
450
|
+
collection: 'snapshots',
|
|
451
|
+
where: { key },
|
|
452
|
+
orderBy: { field: 'timestamp', direction: 'asc' },
|
|
453
|
+
...(options?.limit ? { limit: options.limit } : {}),
|
|
454
|
+
...(options?.offset ? { offset: options.offset } : {}),
|
|
455
|
+
});
|
|
456
|
+
return records;
|
|
457
|
+
}
|
|
458
|
+
/** Get the latest snapshot for a key */
|
|
459
|
+
async latest(key) {
|
|
460
|
+
const records = await this.adapter.query({
|
|
461
|
+
collection: 'snapshots',
|
|
462
|
+
where: { key },
|
|
463
|
+
orderBy: { field: 'timestamp', direction: 'desc' },
|
|
464
|
+
limit: 1,
|
|
465
|
+
});
|
|
466
|
+
return records[0] ?? undefined;
|
|
467
|
+
}
|
|
468
|
+
/** Get snapshot closest to a specific time */
|
|
469
|
+
async at(key, time) {
|
|
470
|
+
const all = await this.history(key);
|
|
471
|
+
if (!all.length)
|
|
472
|
+
return undefined;
|
|
473
|
+
const targetMs = time.getTime();
|
|
474
|
+
let closest;
|
|
475
|
+
let minDiff = Infinity;
|
|
476
|
+
for (const snap of all) {
|
|
477
|
+
const diff = Math.abs(new Date(snap.timestamp).getTime() - targetMs);
|
|
478
|
+
if (diff < minDiff) {
|
|
479
|
+
minDiff = diff;
|
|
480
|
+
closest = snap;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return closest;
|
|
484
|
+
}
|
|
485
|
+
/** Diff two snapshots */
|
|
486
|
+
diff(older, newer) {
|
|
487
|
+
const olderKeys = new Set(Object.keys(older.data));
|
|
488
|
+
const newerKeys = new Set(Object.keys(newer.data));
|
|
489
|
+
const added = [];
|
|
490
|
+
const removed = [];
|
|
491
|
+
const changed = [];
|
|
492
|
+
for (const key of newerKeys) {
|
|
493
|
+
if (!olderKeys.has(key)) {
|
|
494
|
+
added.push(key);
|
|
495
|
+
}
|
|
496
|
+
else if (JSON.stringify(older.data[key]) !== JSON.stringify(newer.data[key])) {
|
|
497
|
+
changed.push({ key, from: older.data[key], to: newer.data[key] });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
for (const key of olderKeys) {
|
|
501
|
+
if (!newerKeys.has(key))
|
|
502
|
+
removed.push(key);
|
|
503
|
+
}
|
|
504
|
+
return { added, removed, changed };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// ─── Correlation API ─────────────────────────────────
|
|
508
|
+
class CorrelationAPI {
|
|
509
|
+
adapter;
|
|
510
|
+
constructor(adapter) {
|
|
511
|
+
this.adapter = adapter;
|
|
512
|
+
}
|
|
513
|
+
/** Track a metric value */
|
|
514
|
+
async track(metricId, value, options) {
|
|
515
|
+
let metric = await this.adapter.get('metrics', metricId);
|
|
516
|
+
const point = {
|
|
517
|
+
timestamp: new Date(),
|
|
518
|
+
value,
|
|
519
|
+
source: options?.source || 'local',
|
|
520
|
+
confidence: 1.0,
|
|
521
|
+
};
|
|
522
|
+
if (!metric) {
|
|
523
|
+
metric = {
|
|
524
|
+
id: metricId,
|
|
525
|
+
entityId: options?.entityId || metricId,
|
|
526
|
+
name: options?.name || metricId,
|
|
527
|
+
unit: options?.unit,
|
|
528
|
+
points: [point],
|
|
529
|
+
aggregation: 'last',
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
metric.points.push(point);
|
|
534
|
+
metric.points.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
535
|
+
// Cap points to prevent unbounded growth (default: 10,000)
|
|
536
|
+
const MAX_POINTS = 10_000;
|
|
537
|
+
if (metric.points.length > MAX_POINTS) {
|
|
538
|
+
// Keep the most recent MAX_POINTS entries
|
|
539
|
+
metric.points = metric.points.slice(-MAX_POINTS);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
await this.adapter.put('metrics', metricId, metric);
|
|
543
|
+
}
|
|
544
|
+
/** Find correlations for a metric */
|
|
545
|
+
async find(metricId, options) {
|
|
546
|
+
const target = await this.adapter.get('metrics', metricId);
|
|
547
|
+
if (!target || target.points.length < 10)
|
|
548
|
+
return [];
|
|
549
|
+
const allMetrics = await this.adapter.query({ collection: 'metrics' });
|
|
550
|
+
const minCorr = options?.minCorrelation ?? 0.5;
|
|
551
|
+
const maxLag = options?.maxLagDays ?? 14;
|
|
552
|
+
const results = [];
|
|
553
|
+
for (const other of allMetrics) {
|
|
554
|
+
if (other.id === metricId || other.points.length < 10)
|
|
555
|
+
continue;
|
|
556
|
+
for (let lag = 0; lag <= maxLag; lag++) {
|
|
557
|
+
const r = this.pearson(target, other, lag);
|
|
558
|
+
if (r !== null && Math.abs(r.correlation) >= minCorr) {
|
|
559
|
+
results.push({
|
|
560
|
+
metricId: other.id,
|
|
561
|
+
correlation: r.correlation,
|
|
562
|
+
lagDays: lag,
|
|
563
|
+
direction: r.correlation >= 0 ? 'positive' : 'negative',
|
|
564
|
+
sampleSize: r.n,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return results.sort((a, b) => Math.abs(b.correlation) - Math.abs(a.correlation));
|
|
570
|
+
}
|
|
571
|
+
/** Get a metric's history */
|
|
572
|
+
async get(metricId) {
|
|
573
|
+
return await this.adapter.get('metrics', metricId);
|
|
574
|
+
}
|
|
575
|
+
/** List all tracked metrics */
|
|
576
|
+
async list() {
|
|
577
|
+
return await this.adapter.query({ collection: 'metrics' });
|
|
578
|
+
}
|
|
579
|
+
// ── Private ──
|
|
580
|
+
pearson(a, b, lagDays) {
|
|
581
|
+
// Align time series at daily granularity
|
|
582
|
+
const dayMs = 86400000;
|
|
583
|
+
const aMap = new Map();
|
|
584
|
+
for (const p of a.points) {
|
|
585
|
+
const day = Math.floor(new Date(p.timestamp).getTime() / dayMs);
|
|
586
|
+
aMap.set(day, p.value);
|
|
587
|
+
}
|
|
588
|
+
const pairs = [];
|
|
589
|
+
for (const p of b.points) {
|
|
590
|
+
const day = Math.floor(new Date(p.timestamp).getTime() / dayMs) + lagDays;
|
|
591
|
+
const aVal = aMap.get(day);
|
|
592
|
+
if (aVal !== undefined)
|
|
593
|
+
pairs.push([aVal, p.value]);
|
|
594
|
+
}
|
|
595
|
+
if (pairs.length < 5)
|
|
596
|
+
return null;
|
|
597
|
+
const n = pairs.length;
|
|
598
|
+
const meanA = pairs.reduce((s, p) => s + p[0], 0) / n;
|
|
599
|
+
const meanB = pairs.reduce((s, p) => s + p[1], 0) / n;
|
|
600
|
+
let num = 0, denA = 0, denB = 0;
|
|
601
|
+
for (const [a, b] of pairs) {
|
|
602
|
+
const da = a - meanA;
|
|
603
|
+
const db = b - meanB;
|
|
604
|
+
num += da * db;
|
|
605
|
+
denA += da * da;
|
|
606
|
+
denB += db * db;
|
|
607
|
+
}
|
|
608
|
+
const den = Math.sqrt(denA * denB);
|
|
609
|
+
if (den === 0)
|
|
610
|
+
return null;
|
|
611
|
+
return { correlation: num / den, n };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// ─── Vector API ──────────────────────────────────────
|
|
615
|
+
class VectorAPI {
|
|
616
|
+
adapter;
|
|
617
|
+
constructor(adapter) {
|
|
618
|
+
this.adapter = adapter;
|
|
619
|
+
}
|
|
620
|
+
get vec() {
|
|
621
|
+
return 'putVector' in this.adapter ? this.adapter : null;
|
|
622
|
+
}
|
|
623
|
+
/** Check if vector search is available */
|
|
624
|
+
get available() {
|
|
625
|
+
return this.vec !== null;
|
|
626
|
+
}
|
|
627
|
+
/** Store a vector */
|
|
628
|
+
async store(collection, key, vector, metadata) {
|
|
629
|
+
const v = this.vec;
|
|
630
|
+
if (!v)
|
|
631
|
+
throw new AdapterCapabilityError('vector.store()', 'a VectorAdapter (use MemoryVectorAdapter or VectorOverlay)');
|
|
632
|
+
await v.putVector(collection, key, vector, metadata);
|
|
633
|
+
}
|
|
634
|
+
/** Search by vector similarity */
|
|
635
|
+
async search(collection, vector, topK = 10, minScore) {
|
|
636
|
+
const v = this.vec;
|
|
637
|
+
if (!v)
|
|
638
|
+
throw new AdapterCapabilityError('vector.search()', 'a VectorAdapter (use MemoryVectorAdapter or VectorOverlay)');
|
|
639
|
+
return v.searchVector(collection, vector, topK, minScore);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// ─── Factory Function ────────────────────────────────
|
|
643
|
+
/**
|
|
644
|
+
* Create a dpth database instance.
|
|
645
|
+
*
|
|
646
|
+
* @param pathOrOptions — SQLite path string, or options object
|
|
647
|
+
* @returns Initialized Dpth instance (call .ready() if you need to await init)
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* // In-memory (default)
|
|
651
|
+
* const db = dpth();
|
|
652
|
+
*
|
|
653
|
+
* // Persistent (SQLite)
|
|
654
|
+
* const db = await dpth('./myapp.db').ready();
|
|
655
|
+
*
|
|
656
|
+
* // Custom adapter
|
|
657
|
+
* const db = dpth({ adapter: new MemoryVectorAdapter() });
|
|
658
|
+
*/
|
|
659
|
+
export function dpth(pathOrOptions) {
|
|
660
|
+
if (typeof pathOrOptions === 'string') {
|
|
661
|
+
return new Dpth({ path: pathOrOptions });
|
|
662
|
+
}
|
|
663
|
+
return new Dpth(pathOrOptions);
|
|
664
|
+
}
|
|
665
|
+
//# sourceMappingURL=dpth.js.map
|