@equationalapplications/core-llm-wiki 3.0.0 → 3.2.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 +192 -1
- package/dist/index.d.mts +164 -7
- package/dist/index.d.ts +164 -7
- package/dist/index.js +664 -86
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +664 -87
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,13 +124,171 @@ const memory = await wikiMemory.read('user-123', 'my preferences', {
|
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
**Hybrid scoring blends:**
|
|
127
|
-
- `hybridWeight: 1.0` →
|
|
127
|
+
- `hybridWeight: 1.0` → all-semantic blend with semantic scores clamped to non-negative range (no keyword component)
|
|
128
128
|
- `hybridWeight: 0.5` → balanced semantic + keyword (50/50 blend)
|
|
129
129
|
- `hybridWeight: 0.0` → pure keyword ranking, skips `embed()` entirely (no LLM API cost)
|
|
130
130
|
|
|
131
|
+
True cosine-range pure semantic ranking (including negative cosine values) is used when `hybridWeight` is left `undefined`.
|
|
132
|
+
|
|
131
133
|
**Pre-filtering optimization:**
|
|
132
134
|
When `preFilterLimit: 50` is set with 1000 facts, cosine similarity is computed only for the top 50 MiniSearch keyword matches, reducing O(N) scoring to O(50).
|
|
133
135
|
|
|
136
|
+
## Pluggable Vector Retrieval
|
|
137
|
+
|
|
138
|
+
When your entity corpus grows, in-process cosine similarity scoring becomes a bottleneck. The optional **`VectorRanker`** interface lets you delegate semantic ranking to **sqlite-vec**, **sqlite-vss**, or an external vector database while `WikiMemory` handles embedding validation, hybrid scoring, and tier-2 row hydration.
|
|
139
|
+
|
|
140
|
+
### `VectorRanker` purpose
|
|
141
|
+
|
|
142
|
+
`VectorRanker` provides an optional injection point for approximate nearest-neighbor (ANN) ranking:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
export interface VectorRanker {
|
|
146
|
+
/**
|
|
147
|
+
* Return semantic scores for facts in scope, sorted by similarity.
|
|
148
|
+
* - `entityId`: restricts results to one entity
|
|
149
|
+
* - `queryVec`: the embedded query (Float32Array or number[])
|
|
150
|
+
* - `candidateIds` (optional): when set, rank only within this set (MiniSearch pre-filter mode)
|
|
151
|
+
* - `limit`: requested top-K count
|
|
152
|
+
*/
|
|
153
|
+
rankBySimilarity(args: VectorRankerRankArgs): Promise<VectorRankerSemanticResult[]>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Optional hook called after embedding persistence (upsert, reembed, delete).
|
|
157
|
+
* Implementations use this to keep external indexes (sqlite-vec, remote ANN) in sync.
|
|
158
|
+
*/
|
|
159
|
+
onEmbeddingPersisted?(event: {
|
|
160
|
+
entityId: string;
|
|
161
|
+
factId: string;
|
|
162
|
+
vector: Float32Array | null; // null = embedding removed
|
|
163
|
+
}): void | Promise<void>;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**When no ranker is configured**, `WikiMemory` uses built-in JS cosine similarity — the same behavior as today. When a ranker is supplied and embeddings preconditions are met (`embed` available, dimensions match, no mismatches), `WikiMemory` delegates scoring to the ranker and blends results with keyword scores.
|
|
168
|
+
|
|
169
|
+
### Example: sqlite-vec adapter
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { WikiMemory } from '@equationalapplications/core-llm-wiki';
|
|
173
|
+
import type { VectorRanker, VectorRankerRankArgs, VectorRankerSemanticResult } from '@equationalapplications/core-llm-wiki';
|
|
174
|
+
|
|
175
|
+
// Minimal sqlite-vec adapter (pseudo-code)
|
|
176
|
+
const sqliteVecRanker: VectorRanker = {
|
|
177
|
+
async rankBySimilarity(args: VectorRankerRankArgs): Promise<VectorRankerSemanticResult[]> {
|
|
178
|
+
const { entityId, queryVec, candidateIds, limit } = args;
|
|
179
|
+
|
|
180
|
+
// Build KNN query using sqlite-vec's distance functions.
|
|
181
|
+
// sqlite-vec returns cosine distance (0 = identical, 2 = opposite) ascending.
|
|
182
|
+
// Invert to semanticScore: higher = more similar, matching VectorRanker contract.
|
|
183
|
+
let sql = `SELECT id, (1.0 - distance) AS semanticScore FROM vec_facts
|
|
184
|
+
WHERE entity_id = ? AND deleted_at IS NULL`;
|
|
185
|
+
const params: any[] = [entityId];
|
|
186
|
+
|
|
187
|
+
// Apply pre-filter if provided
|
|
188
|
+
if (candidateIds) {
|
|
189
|
+
sql += ` AND id IN (${candidateIds.map(() => '?').join(',')})`;
|
|
190
|
+
params.push(...candidateIds);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// KNN search (example syntax; adjust for your sqlite-vec version)
|
|
194
|
+
sql += ` ORDER BY vec MATCH vec_neighbor(?) LIMIT ?`;
|
|
195
|
+
params.push(queryVec, limit);
|
|
196
|
+
|
|
197
|
+
const rows = await db.getAllAsync<{ id: string; semanticScore: number }>(sql, params);
|
|
198
|
+
return rows; // sorted descending by semanticScore (closest distance → highest similarity)
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async onEmbeddingPersisted(event) {
|
|
202
|
+
const { entityId, factId, vector } = event;
|
|
203
|
+
if (vector) {
|
|
204
|
+
// Upsert into sqlite-vec table
|
|
205
|
+
await db.runAsync(
|
|
206
|
+
`INSERT OR REPLACE INTO vec_facts (id, entity_id, vec) VALUES (?, ?, ?)`,
|
|
207
|
+
[factId, entityId, vector]
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
// Delete when embedding is removed
|
|
211
|
+
await db.runAsync(`DELETE FROM vec_facts WHERE id = ?`, [factId]);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const wikiMemory = new WikiMemory(db, {
|
|
217
|
+
llmProvider: { /* ... */ },
|
|
218
|
+
vectorRanker: sqliteVecRanker,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// read() now uses sqlite-vec for scoring instead of JS cosine
|
|
222
|
+
const memory = await wikiMemory.read('user-123', 'my preferences');
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Fallback policies
|
|
226
|
+
|
|
227
|
+
When `rankBySimilarity` rejects (e.g., ANN service outage, misconfiguration), `WikiMemory` applies a recovery policy:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
export type VectorRankerFallback =
|
|
231
|
+
| 'js-cosine' // (default) Score candidates in-process with JS cosine — same as no ranker
|
|
232
|
+
| 'keyword' // Skip semantic ranking; return keyword-only results
|
|
233
|
+
| 'empty' // Semantic facts list empty for this read; tasks/events still included
|
|
234
|
+
| 'throw'; // Reject read() with the ranker error
|
|
235
|
+
|
|
236
|
+
const wikiMemory = new WikiMemory(db, {
|
|
237
|
+
llmProvider: { /* ... */ },
|
|
238
|
+
vectorRanker: sqliteVecRanker,
|
|
239
|
+
vectorRankerFallback: 'js-cosine', // default
|
|
240
|
+
onVectorRankerFallback: (info) => {
|
|
241
|
+
console.warn(
|
|
242
|
+
`Ranker failed (policy: ${info.policy}); error:`,
|
|
243
|
+
info.error
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- **`'js-cosine'` (default):** Seamless degradation; same behavior as if no ranker was configured.
|
|
250
|
+
- **`'keyword'`:** Useful when semantic ranking is optional; keyword search proceeds normally.
|
|
251
|
+
- **`'empty'`:** Return no facts for this query (but tasks/events still load); useful for strict consistency.
|
|
252
|
+
- **`'throw'`:** Propagate the error and fail the read.
|
|
253
|
+
|
|
254
|
+
### `onEmbeddingPersisted` eventual consistency
|
|
255
|
+
|
|
256
|
+
If `vectorRanker.onEmbeddingPersisted` returns a pending Promise, the hook **may resolve asynchronously**. This supports ANN indexes that rebuild on a schedule (e.g., sqlite-vec triggers on transaction commit) or external services with eventual consistency.
|
|
257
|
+
|
|
258
|
+
**Best practice:**
|
|
259
|
+
- If your adapter has **synchronous guarantees** (in-process sqlite-vec, same transaction), await the promise.
|
|
260
|
+
- If your adapter is **eventually consistent** (remote ANN, async rebuild), document the lag and document that queries may miss recently-added facts until the index refreshes.
|
|
261
|
+
- The **SQLite blob remains the source of truth**; `WikiMemory` always writes embeddings to `embedding_blob` first before calling the hook.
|
|
262
|
+
|
|
263
|
+
### Hybrid scoring with ranker
|
|
264
|
+
|
|
265
|
+
When both `vectorRanker` and `hybridWeight` are configured, `WikiMemory` still applies hybrid blending after the ranker returns scores:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const wikiMemory = new WikiMemory(db, {
|
|
269
|
+
config: {
|
|
270
|
+
hybridWeight: 0.7, // 70% semantic, 30% keyword
|
|
271
|
+
},
|
|
272
|
+
vectorRanker: sqliteVecRanker,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ranker returns semanticScore; WikiMemory blends with MiniSearch keyword score
|
|
276
|
+
const memory = await wikiMemory.read('user-123', 'my preferences', {
|
|
277
|
+
hybridWeight: 0.5, // per-call override to 50/50 blend
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Note on semantics:
|
|
282
|
+
- Leave `hybridWeight` undefined for true pure-semantic cosine-range scoring.
|
|
283
|
+
- Set `hybridWeight: 1` for an all-semantic variant that clamps negative semantic scores to 0.
|
|
284
|
+
|
|
285
|
+
For details on hybrid scoring formulas and trade-offs, see [Retrieval Tuning](#retrieval-tuning) above.
|
|
286
|
+
|
|
287
|
+
### Spec and issue reference
|
|
288
|
+
|
|
289
|
+
- **Full spec:** [`docs/superpowers/specs/2026-05-07-pluggable-vector-retrieval.md`](https://github.com/equationalapplications/expo-llm-wiki/blob/main/docs/superpowers/specs/2026-05-07-pluggable-vector-retrieval.md)
|
|
290
|
+
- **GitHub issue:** [#15](https://github.com/equationalapplications/expo-llm-wiki/issues/15)
|
|
291
|
+
|
|
134
292
|
## Vector Cache
|
|
135
293
|
|
|
136
294
|
Parsed embedding vectors from full-scan `read()` calls are cached in memory, keyed by entity ID (max 16 entities, max 500 vectors per entity). This avoids redundant `Float32Array` parsing on repeated queries for the same entity. When the 16-entity limit is reached, the oldest-inserted entity is evicted to make room; if an entity exceeds 500 facts, its vectors are not cached at all for that read.
|
|
@@ -144,6 +302,39 @@ wikiMemory.clearVectorCache();
|
|
|
144
302
|
|
|
145
303
|
The cache is also automatically invalidated on any mutation (`runLibrarian`, `runHeal`, `runPrune`, `runReembed`, `ingestDocument`, `importDump`, `forget`).
|
|
146
304
|
|
|
305
|
+
## Security
|
|
306
|
+
|
|
307
|
+
`@equationalapplications/core-llm-wiki` enforces multiple security layers:
|
|
308
|
+
|
|
309
|
+
### VectorRanker Adapter Security
|
|
310
|
+
|
|
311
|
+
If implementing a custom `VectorRanker`:
|
|
312
|
+
|
|
313
|
+
- **SQL Injection**: ALWAYS use parameterized queries for `entityId`, `factId`, `candidateIds`. Never concatenate into SQL strings.
|
|
314
|
+
- **Entity Isolation**: Filter by `entityId` in all queries to prevent cross-tenant data leaks.
|
|
315
|
+
- **Credential Scrubbing**: Strip API keys, tokens, connection strings from thrown errors before surfacing to host.
|
|
316
|
+
- **Resource Limits**: Cap `limit` and `candidateIds.length` to prevent DoS. Do NOT retain `vector` references beyond callback scope — blocks GC.
|
|
317
|
+
|
|
318
|
+
See [SECURITY.md](../../SECURITY.md) for complete adapter security guidance and code examples.
|
|
319
|
+
|
|
320
|
+
### Host Application Security
|
|
321
|
+
|
|
322
|
+
When using `VectorRanker`:
|
|
323
|
+
|
|
324
|
+
- **Error Sanitization**: `sanitizeRankerErrors: true` (default) scrubs ranker errors before mirroring via `error.cause`.
|
|
325
|
+
- **Fallback Policy**: Choose `vectorRankerFallback` based on availability vs consistency requirements:
|
|
326
|
+
- `'js-cosine'` (default): Best availability
|
|
327
|
+
- `'keyword'`: Fast fallback without semantic ranking
|
|
328
|
+
- `'empty'`: Strict consistency (no facts on failure)
|
|
329
|
+
- `'throw'`: Fail-fast error propagation
|
|
330
|
+
- **Deletion Hook Contract**: `forget()` / `runPrune()` reject on hook timeout/failure. Prevents GDPR violations (deleted vectors still retrievable). Handle failures with retry or queue for reconciliation.
|
|
331
|
+
- **Timeout Tuning**: Set `deletionHookTimeoutMs` per deployment (default 30s). Interactive UX: 5s. Background jobs: 60s.
|
|
332
|
+
|
|
333
|
+
Core WikiMemory provides:
|
|
334
|
+
- **Defensive Copies**: Query/embedding vectors copied before ranker/hook calls
|
|
335
|
+
- **Input Validation**: `sourceRef`/`sourceHash` normalized; embedding dimensions validated
|
|
336
|
+
- **Parameterized Queries**: All SQL uses bind parameters
|
|
337
|
+
|
|
147
338
|
## Usage
|
|
148
339
|
|
|
149
340
|
```typescript
|
package/dist/index.d.mts
CHANGED
|
@@ -132,20 +132,135 @@ interface LLMProvider {
|
|
|
132
132
|
*/
|
|
133
133
|
embed?: (text: string) => Promise<number[]>;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Result of semantic ranking for a single fact.
|
|
137
|
+
*/
|
|
138
|
+
interface VectorRankerSemanticResult {
|
|
139
|
+
id: string;
|
|
140
|
+
/** Cosine similarity in [-1, 1] when exact; implementations MAY document other monotonic scales. */
|
|
141
|
+
semanticScore: number;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Arguments passed to VectorRanker.rankBySimilarity.
|
|
145
|
+
*/
|
|
146
|
+
interface VectorRankerRankArgs {
|
|
147
|
+
entityId: string;
|
|
148
|
+
/**
|
|
149
|
+
* Query embedding. Treat as readonly — core provides a defensive copy,
|
|
150
|
+
* but adapters MUST NOT mutate this array. Mutation can corrupt
|
|
151
|
+
* WikiMemory's internal vector cache and JS-cosine fallback path.
|
|
152
|
+
*/
|
|
153
|
+
queryVec: Float32Array | number[];
|
|
154
|
+
/**
|
|
155
|
+
* When set (MiniSearch pre-filter path): ranker MUST only produce results for ids in this set.
|
|
156
|
+
* When omitted (full-entity semantic path): ranker scopes by entityId per its backing store contract.
|
|
157
|
+
*/
|
|
158
|
+
candidateIds?: readonly string[];
|
|
159
|
+
/**
|
|
160
|
+
* Upper bound on how many distinct fact ids should receive a semanticScore in this call.
|
|
161
|
+
* WikiMemory derives this from maxResults / candidate cardinality / documented oversampling policy.
|
|
162
|
+
*/
|
|
163
|
+
limit: number;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Optional backend for semantic candidate scoring / top-k retrieval.
|
|
167
|
+
* When omitted, WikiMemory scores rows with embedding_blob / embedding TEXT in JS (cosine).
|
|
168
|
+
*/
|
|
169
|
+
interface VectorRanker {
|
|
170
|
+
/**
|
|
171
|
+
* Return semantic scores for facts in scope, sorted descending by semanticScore (stable tie-breaking
|
|
172
|
+
* not required — WikiMemory reapplies existing tie-breakers after blending).
|
|
173
|
+
* Implementations SHOULD omit facts with no usable vector; callers treat missing ids like today's
|
|
174
|
+
* "no embedding" rows (pure semantic: -2; hybrid: keyword-only portion).
|
|
175
|
+
*/
|
|
176
|
+
rankBySimilarity(args: VectorRankerRankArgs): Promise<VectorRankerSemanticResult[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Called after a fact's embedding is successfully persisted to embedding_blob (or cleared).
|
|
179
|
+
* Hosts use this to keep sqlite-vec / external indexes consistent with SQLite as source of truth.
|
|
180
|
+
*
|
|
181
|
+
* On deletion paths (forget, prune, hard-delete), core awaits this hook to ensure ANN cleanup
|
|
182
|
+
* completes before the deletion call resolves (GDPR compliance). Hook failures or timeouts on
|
|
183
|
+
* those paths reject the deletion call.
|
|
184
|
+
*
|
|
185
|
+
* Treat `vector` as readonly — core provides a defensive copy, but adapters MUST NOT mutate.
|
|
186
|
+
*
|
|
187
|
+
* Optional: if omitted, hosts MUST document "index rebuilt separately" and accept stale ANN until rebuild.
|
|
188
|
+
*/
|
|
189
|
+
onEmbeddingPersisted?(event: {
|
|
190
|
+
entityId: string;
|
|
191
|
+
factId: string;
|
|
192
|
+
vector: Float32Array | null;
|
|
193
|
+
}): void | Promise<void>;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Fallback policy when rankBySimilarity rejects.
|
|
197
|
+
*/
|
|
198
|
+
type VectorRankerFallback = 'js-cosine' | 'keyword' | 'empty' | 'throw';
|
|
135
199
|
interface WikiOptions {
|
|
136
200
|
config?: WikiConfig;
|
|
137
201
|
llmProvider: LLMProvider;
|
|
138
202
|
/**
|
|
139
|
-
* Called when embedding-based retrieval is unavailable during `read()
|
|
140
|
-
*
|
|
141
|
-
* - `embed()` throws (e.g. network error, model unavailable)
|
|
142
|
-
* - `embed()` returns a vector with non-finite values (NaN / Infinity)
|
|
203
|
+
* Called when embedding-based retrieval is degraded or unavailable during `read()`.
|
|
204
|
+
* This can happen when:
|
|
205
|
+
* - `embed()` throws (e.g. network error, model unavailable) → falls back to keyword search
|
|
206
|
+
* - `embed()` returns a vector with non-finite values (NaN / Infinity) → falls back to keyword search
|
|
143
207
|
* - The query vector's dimension doesn't match stored embeddings (model switch;
|
|
144
|
-
* resolve by calling `runReembed()`)
|
|
208
|
+
* resolve by calling `runReembed()`) → falls back to keyword search
|
|
209
|
+
* - `vectorRanker` returns IDs that don't belong to the requested entity or don't exist
|
|
210
|
+
* (ranker integrity issue; returned rows will be filtered out, reducing result count) →
|
|
211
|
+
* may still use semantic ranking, but with degraded quality
|
|
145
212
|
*
|
|
146
|
-
* `read()`
|
|
213
|
+
* `read()` returns results (keyword fallback or degraded semantic) — this is a notification, not an error path.
|
|
147
214
|
*/
|
|
148
215
|
onRetrievalFallback?: (error: Error) => void;
|
|
216
|
+
/**
|
|
217
|
+
* Optional backend for semantic candidate scoring / top-k retrieval.
|
|
218
|
+
* When omitted, WikiMemory scores rows with embedding_blob / embedding TEXT in JS (cosine).
|
|
219
|
+
*/
|
|
220
|
+
vectorRanker?: VectorRanker;
|
|
221
|
+
/**
|
|
222
|
+
* When rankBySimilarity throws. Default `'js-cosine'`.
|
|
223
|
+
* Ignored when vectorRanker is undefined.
|
|
224
|
+
*/
|
|
225
|
+
vectorRankerFallback?: VectorRankerFallback;
|
|
226
|
+
/**
|
|
227
|
+
* Called only when rankBySimilarity rejects (after embeddings path succeeded).
|
|
228
|
+
* Invoked before applying vectorRankerFallback when that policy recovers or before rejecting when policy is 'throw'.
|
|
229
|
+
*/
|
|
230
|
+
onVectorRankerFallback?: (info: {
|
|
231
|
+
error: Error;
|
|
232
|
+
/** Effective policy core will apply for this read (same as WikiOptions.vectorRankerFallback, default js-cosine). */
|
|
233
|
+
policy: VectorRankerFallback;
|
|
234
|
+
}) => void;
|
|
235
|
+
/**
|
|
236
|
+
* When true: after rankBySimilarity failure, once the recoverable fallback has finished
|
|
237
|
+
* and read() will resolve, invoke onRetrievalFallback — after onVectorRankerFallback if set.
|
|
238
|
+
* Ignored when vectorRankerFallback is 'throw'. Default false.
|
|
239
|
+
*/
|
|
240
|
+
propagateRankerFailureToRetrievalFallback?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* When true (default), sanitize ranker errors before exposing via error.cause
|
|
243
|
+
* to prevent credential leakage in host telemetry. Disable only when you
|
|
244
|
+
* control the ranker implementation.
|
|
245
|
+
*
|
|
246
|
+
* Sanitization replaces error message/stack with a generic message preserving
|
|
247
|
+
* only the error type (constructor name).
|
|
248
|
+
*/
|
|
249
|
+
sanitizeRankerErrors?: boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Timeout (ms) for onEmbeddingPersisted hook on GDPR deletion paths
|
|
252
|
+
* (forget, _doPrune). Hook must complete within this window or the
|
|
253
|
+
* deletion operation rejects. Default 30000.
|
|
254
|
+
* Lower for interactive deletes; raise for slow remote ANN backends.
|
|
255
|
+
*/
|
|
256
|
+
deletionHookTimeoutMs?: number;
|
|
257
|
+
/**
|
|
258
|
+
* Escape hatch: skip onEmbeddingPersisted on deletion paths entirely.
|
|
259
|
+
* Use ONLY when the ANN backend is permanently decommissioned. Vectors
|
|
260
|
+
* orphaned in the (unreachable) external index are accepted as a tradeoff.
|
|
261
|
+
* NOT GDPR-safe for live indexes. Default false.
|
|
262
|
+
*/
|
|
263
|
+
forceDeleteIgnoreRankerHook?: boolean;
|
|
149
264
|
}
|
|
150
265
|
interface MemoryBundle {
|
|
151
266
|
facts: WikiFact[];
|
|
@@ -198,6 +313,15 @@ declare class WikiBusyError extends Error {
|
|
|
198
313
|
readonly entityId: string;
|
|
199
314
|
constructor(operation: WikiBusyOperation, entityId: string);
|
|
200
315
|
}
|
|
316
|
+
declare class PrunePartialFailureError extends Error {
|
|
317
|
+
readonly deleted: number;
|
|
318
|
+
readonly failedAt: string;
|
|
319
|
+
readonly remaining: number;
|
|
320
|
+
readonly deletedTasks: number;
|
|
321
|
+
readonly deletedEvents: number;
|
|
322
|
+
readonly cause: Error;
|
|
323
|
+
constructor(deleted: number, failedAt: string, remaining: number, cause: Error, deletedTasks?: number, deletedEvents?: number);
|
|
324
|
+
}
|
|
201
325
|
|
|
202
326
|
declare class WikiMemory {
|
|
203
327
|
private db;
|
|
@@ -234,6 +358,13 @@ declare class WikiMemory {
|
|
|
234
358
|
private _librarianKey;
|
|
235
359
|
private _healKey;
|
|
236
360
|
private _warnCrossEntityCollision;
|
|
361
|
+
private _notifyEmbeddingPersisted;
|
|
362
|
+
/**
|
|
363
|
+
* GDPR-critical variant: awaits the hook with a timeout and rethrows failures.
|
|
364
|
+
* Use ONLY on deletion paths. forget() calls after soft-delete UPDATE; runPrune()
|
|
365
|
+
* calls before hard DELETE. For best-effort sync, use _notifyEmbeddingPersisted.
|
|
366
|
+
*/
|
|
367
|
+
private _notifyEmbeddingPersistedOrThrow;
|
|
237
368
|
constructor(db: SQLiteAdapter, options: WikiOptions);
|
|
238
369
|
setup(): Promise<void>;
|
|
239
370
|
hasChanged(entityId: string, sourceRef: string, sourceHash: string): Promise<boolean>;
|
|
@@ -261,6 +392,32 @@ declare class WikiMemory {
|
|
|
261
392
|
events: number;
|
|
262
393
|
}>;
|
|
263
394
|
read(entityId: string, query: string, options?: ReadOptions): Promise<MemoryBundle>;
|
|
395
|
+
/**
|
|
396
|
+
* Stable tie-break sort: score desc → access_count desc → updated_at desc → id asc.
|
|
397
|
+
*/
|
|
398
|
+
private _tieBreakSort;
|
|
399
|
+
/**
|
|
400
|
+
* Comparator for score + deterministic tie-break fields.
|
|
401
|
+
* Negative return means "a ranks ahead of b" for descending score order.
|
|
402
|
+
*/
|
|
403
|
+
private _compareScoredRows;
|
|
404
|
+
/**
|
|
405
|
+
* Strip potentially sensitive data from ranker errors before exposing to host callbacks.
|
|
406
|
+
* Preserves error type for debugging but removes message/stack that may contain credentials.
|
|
407
|
+
* Recursively sanitizes one level of .cause; deeper chains collapse to type only.
|
|
408
|
+
*/
|
|
409
|
+
private _sanitizeRankerError;
|
|
410
|
+
/**
|
|
411
|
+
* Score candidate rows using in-process JS cosine similarity.
|
|
412
|
+
* Applies hybrid blending (if weight set) and tie-break sorting before returning.
|
|
413
|
+
*/
|
|
414
|
+
private _rankWithJsCosine;
|
|
415
|
+
/**
|
|
416
|
+
* Delegate semantic ranking to the injected VectorRanker.
|
|
417
|
+
* Caller should pass an oversampledLimit to preserve recall after re-ranking.
|
|
418
|
+
* Returns scored results ready for hybrid blending and tie-break sorting.
|
|
419
|
+
*/
|
|
420
|
+
private _rankWithVectorRanker;
|
|
264
421
|
getMemoryBundle(entityId: string): Promise<MemoryBundle>;
|
|
265
422
|
write(entityId: string, event: Omit<WikiEvent, 'id' | 'entity_id' | 'created_at'>): Promise<void>;
|
|
266
423
|
private runLibrarianThenMaybeHeal;
|
|
@@ -315,4 +472,4 @@ declare function formatMemoryDump(dump: MemoryDump): FormattedMemoryDump;
|
|
|
315
472
|
|
|
316
473
|
declare function createWiki(db: SQLiteAdapter, options: WikiOptions): WikiMemory;
|
|
317
474
|
|
|
318
|
-
export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, type ReadOptions, type SQLiteAdapter, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump };
|
|
475
|
+
export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump };
|
package/dist/index.d.ts
CHANGED
|
@@ -132,20 +132,135 @@ interface LLMProvider {
|
|
|
132
132
|
*/
|
|
133
133
|
embed?: (text: string) => Promise<number[]>;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Result of semantic ranking for a single fact.
|
|
137
|
+
*/
|
|
138
|
+
interface VectorRankerSemanticResult {
|
|
139
|
+
id: string;
|
|
140
|
+
/** Cosine similarity in [-1, 1] when exact; implementations MAY document other monotonic scales. */
|
|
141
|
+
semanticScore: number;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Arguments passed to VectorRanker.rankBySimilarity.
|
|
145
|
+
*/
|
|
146
|
+
interface VectorRankerRankArgs {
|
|
147
|
+
entityId: string;
|
|
148
|
+
/**
|
|
149
|
+
* Query embedding. Treat as readonly — core provides a defensive copy,
|
|
150
|
+
* but adapters MUST NOT mutate this array. Mutation can corrupt
|
|
151
|
+
* WikiMemory's internal vector cache and JS-cosine fallback path.
|
|
152
|
+
*/
|
|
153
|
+
queryVec: Float32Array | number[];
|
|
154
|
+
/**
|
|
155
|
+
* When set (MiniSearch pre-filter path): ranker MUST only produce results for ids in this set.
|
|
156
|
+
* When omitted (full-entity semantic path): ranker scopes by entityId per its backing store contract.
|
|
157
|
+
*/
|
|
158
|
+
candidateIds?: readonly string[];
|
|
159
|
+
/**
|
|
160
|
+
* Upper bound on how many distinct fact ids should receive a semanticScore in this call.
|
|
161
|
+
* WikiMemory derives this from maxResults / candidate cardinality / documented oversampling policy.
|
|
162
|
+
*/
|
|
163
|
+
limit: number;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Optional backend for semantic candidate scoring / top-k retrieval.
|
|
167
|
+
* When omitted, WikiMemory scores rows with embedding_blob / embedding TEXT in JS (cosine).
|
|
168
|
+
*/
|
|
169
|
+
interface VectorRanker {
|
|
170
|
+
/**
|
|
171
|
+
* Return semantic scores for facts in scope, sorted descending by semanticScore (stable tie-breaking
|
|
172
|
+
* not required — WikiMemory reapplies existing tie-breakers after blending).
|
|
173
|
+
* Implementations SHOULD omit facts with no usable vector; callers treat missing ids like today's
|
|
174
|
+
* "no embedding" rows (pure semantic: -2; hybrid: keyword-only portion).
|
|
175
|
+
*/
|
|
176
|
+
rankBySimilarity(args: VectorRankerRankArgs): Promise<VectorRankerSemanticResult[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Called after a fact's embedding is successfully persisted to embedding_blob (or cleared).
|
|
179
|
+
* Hosts use this to keep sqlite-vec / external indexes consistent with SQLite as source of truth.
|
|
180
|
+
*
|
|
181
|
+
* On deletion paths (forget, prune, hard-delete), core awaits this hook to ensure ANN cleanup
|
|
182
|
+
* completes before the deletion call resolves (GDPR compliance). Hook failures or timeouts on
|
|
183
|
+
* those paths reject the deletion call.
|
|
184
|
+
*
|
|
185
|
+
* Treat `vector` as readonly — core provides a defensive copy, but adapters MUST NOT mutate.
|
|
186
|
+
*
|
|
187
|
+
* Optional: if omitted, hosts MUST document "index rebuilt separately" and accept stale ANN until rebuild.
|
|
188
|
+
*/
|
|
189
|
+
onEmbeddingPersisted?(event: {
|
|
190
|
+
entityId: string;
|
|
191
|
+
factId: string;
|
|
192
|
+
vector: Float32Array | null;
|
|
193
|
+
}): void | Promise<void>;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Fallback policy when rankBySimilarity rejects.
|
|
197
|
+
*/
|
|
198
|
+
type VectorRankerFallback = 'js-cosine' | 'keyword' | 'empty' | 'throw';
|
|
135
199
|
interface WikiOptions {
|
|
136
200
|
config?: WikiConfig;
|
|
137
201
|
llmProvider: LLMProvider;
|
|
138
202
|
/**
|
|
139
|
-
* Called when embedding-based retrieval is unavailable during `read()
|
|
140
|
-
*
|
|
141
|
-
* - `embed()` throws (e.g. network error, model unavailable)
|
|
142
|
-
* - `embed()` returns a vector with non-finite values (NaN / Infinity)
|
|
203
|
+
* Called when embedding-based retrieval is degraded or unavailable during `read()`.
|
|
204
|
+
* This can happen when:
|
|
205
|
+
* - `embed()` throws (e.g. network error, model unavailable) → falls back to keyword search
|
|
206
|
+
* - `embed()` returns a vector with non-finite values (NaN / Infinity) → falls back to keyword search
|
|
143
207
|
* - The query vector's dimension doesn't match stored embeddings (model switch;
|
|
144
|
-
* resolve by calling `runReembed()`)
|
|
208
|
+
* resolve by calling `runReembed()`) → falls back to keyword search
|
|
209
|
+
* - `vectorRanker` returns IDs that don't belong to the requested entity or don't exist
|
|
210
|
+
* (ranker integrity issue; returned rows will be filtered out, reducing result count) →
|
|
211
|
+
* may still use semantic ranking, but with degraded quality
|
|
145
212
|
*
|
|
146
|
-
* `read()`
|
|
213
|
+
* `read()` returns results (keyword fallback or degraded semantic) — this is a notification, not an error path.
|
|
147
214
|
*/
|
|
148
215
|
onRetrievalFallback?: (error: Error) => void;
|
|
216
|
+
/**
|
|
217
|
+
* Optional backend for semantic candidate scoring / top-k retrieval.
|
|
218
|
+
* When omitted, WikiMemory scores rows with embedding_blob / embedding TEXT in JS (cosine).
|
|
219
|
+
*/
|
|
220
|
+
vectorRanker?: VectorRanker;
|
|
221
|
+
/**
|
|
222
|
+
* When rankBySimilarity throws. Default `'js-cosine'`.
|
|
223
|
+
* Ignored when vectorRanker is undefined.
|
|
224
|
+
*/
|
|
225
|
+
vectorRankerFallback?: VectorRankerFallback;
|
|
226
|
+
/**
|
|
227
|
+
* Called only when rankBySimilarity rejects (after embeddings path succeeded).
|
|
228
|
+
* Invoked before applying vectorRankerFallback when that policy recovers or before rejecting when policy is 'throw'.
|
|
229
|
+
*/
|
|
230
|
+
onVectorRankerFallback?: (info: {
|
|
231
|
+
error: Error;
|
|
232
|
+
/** Effective policy core will apply for this read (same as WikiOptions.vectorRankerFallback, default js-cosine). */
|
|
233
|
+
policy: VectorRankerFallback;
|
|
234
|
+
}) => void;
|
|
235
|
+
/**
|
|
236
|
+
* When true: after rankBySimilarity failure, once the recoverable fallback has finished
|
|
237
|
+
* and read() will resolve, invoke onRetrievalFallback — after onVectorRankerFallback if set.
|
|
238
|
+
* Ignored when vectorRankerFallback is 'throw'. Default false.
|
|
239
|
+
*/
|
|
240
|
+
propagateRankerFailureToRetrievalFallback?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* When true (default), sanitize ranker errors before exposing via error.cause
|
|
243
|
+
* to prevent credential leakage in host telemetry. Disable only when you
|
|
244
|
+
* control the ranker implementation.
|
|
245
|
+
*
|
|
246
|
+
* Sanitization replaces error message/stack with a generic message preserving
|
|
247
|
+
* only the error type (constructor name).
|
|
248
|
+
*/
|
|
249
|
+
sanitizeRankerErrors?: boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Timeout (ms) for onEmbeddingPersisted hook on GDPR deletion paths
|
|
252
|
+
* (forget, _doPrune). Hook must complete within this window or the
|
|
253
|
+
* deletion operation rejects. Default 30000.
|
|
254
|
+
* Lower for interactive deletes; raise for slow remote ANN backends.
|
|
255
|
+
*/
|
|
256
|
+
deletionHookTimeoutMs?: number;
|
|
257
|
+
/**
|
|
258
|
+
* Escape hatch: skip onEmbeddingPersisted on deletion paths entirely.
|
|
259
|
+
* Use ONLY when the ANN backend is permanently decommissioned. Vectors
|
|
260
|
+
* orphaned in the (unreachable) external index are accepted as a tradeoff.
|
|
261
|
+
* NOT GDPR-safe for live indexes. Default false.
|
|
262
|
+
*/
|
|
263
|
+
forceDeleteIgnoreRankerHook?: boolean;
|
|
149
264
|
}
|
|
150
265
|
interface MemoryBundle {
|
|
151
266
|
facts: WikiFact[];
|
|
@@ -198,6 +313,15 @@ declare class WikiBusyError extends Error {
|
|
|
198
313
|
readonly entityId: string;
|
|
199
314
|
constructor(operation: WikiBusyOperation, entityId: string);
|
|
200
315
|
}
|
|
316
|
+
declare class PrunePartialFailureError extends Error {
|
|
317
|
+
readonly deleted: number;
|
|
318
|
+
readonly failedAt: string;
|
|
319
|
+
readonly remaining: number;
|
|
320
|
+
readonly deletedTasks: number;
|
|
321
|
+
readonly deletedEvents: number;
|
|
322
|
+
readonly cause: Error;
|
|
323
|
+
constructor(deleted: number, failedAt: string, remaining: number, cause: Error, deletedTasks?: number, deletedEvents?: number);
|
|
324
|
+
}
|
|
201
325
|
|
|
202
326
|
declare class WikiMemory {
|
|
203
327
|
private db;
|
|
@@ -234,6 +358,13 @@ declare class WikiMemory {
|
|
|
234
358
|
private _librarianKey;
|
|
235
359
|
private _healKey;
|
|
236
360
|
private _warnCrossEntityCollision;
|
|
361
|
+
private _notifyEmbeddingPersisted;
|
|
362
|
+
/**
|
|
363
|
+
* GDPR-critical variant: awaits the hook with a timeout and rethrows failures.
|
|
364
|
+
* Use ONLY on deletion paths. forget() calls after soft-delete UPDATE; runPrune()
|
|
365
|
+
* calls before hard DELETE. For best-effort sync, use _notifyEmbeddingPersisted.
|
|
366
|
+
*/
|
|
367
|
+
private _notifyEmbeddingPersistedOrThrow;
|
|
237
368
|
constructor(db: SQLiteAdapter, options: WikiOptions);
|
|
238
369
|
setup(): Promise<void>;
|
|
239
370
|
hasChanged(entityId: string, sourceRef: string, sourceHash: string): Promise<boolean>;
|
|
@@ -261,6 +392,32 @@ declare class WikiMemory {
|
|
|
261
392
|
events: number;
|
|
262
393
|
}>;
|
|
263
394
|
read(entityId: string, query: string, options?: ReadOptions): Promise<MemoryBundle>;
|
|
395
|
+
/**
|
|
396
|
+
* Stable tie-break sort: score desc → access_count desc → updated_at desc → id asc.
|
|
397
|
+
*/
|
|
398
|
+
private _tieBreakSort;
|
|
399
|
+
/**
|
|
400
|
+
* Comparator for score + deterministic tie-break fields.
|
|
401
|
+
* Negative return means "a ranks ahead of b" for descending score order.
|
|
402
|
+
*/
|
|
403
|
+
private _compareScoredRows;
|
|
404
|
+
/**
|
|
405
|
+
* Strip potentially sensitive data from ranker errors before exposing to host callbacks.
|
|
406
|
+
* Preserves error type for debugging but removes message/stack that may contain credentials.
|
|
407
|
+
* Recursively sanitizes one level of .cause; deeper chains collapse to type only.
|
|
408
|
+
*/
|
|
409
|
+
private _sanitizeRankerError;
|
|
410
|
+
/**
|
|
411
|
+
* Score candidate rows using in-process JS cosine similarity.
|
|
412
|
+
* Applies hybrid blending (if weight set) and tie-break sorting before returning.
|
|
413
|
+
*/
|
|
414
|
+
private _rankWithJsCosine;
|
|
415
|
+
/**
|
|
416
|
+
* Delegate semantic ranking to the injected VectorRanker.
|
|
417
|
+
* Caller should pass an oversampledLimit to preserve recall after re-ranking.
|
|
418
|
+
* Returns scored results ready for hybrid blending and tie-break sorting.
|
|
419
|
+
*/
|
|
420
|
+
private _rankWithVectorRanker;
|
|
264
421
|
getMemoryBundle(entityId: string): Promise<MemoryBundle>;
|
|
265
422
|
write(entityId: string, event: Omit<WikiEvent, 'id' | 'entity_id' | 'created_at'>): Promise<void>;
|
|
266
423
|
private runLibrarianThenMaybeHeal;
|
|
@@ -315,4 +472,4 @@ declare function formatMemoryDump(dump: MemoryDump): FormattedMemoryDump;
|
|
|
315
472
|
|
|
316
473
|
declare function createWiki(db: SQLiteAdapter, options: WikiOptions): WikiMemory;
|
|
317
474
|
|
|
318
|
-
export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, type ReadOptions, type SQLiteAdapter, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump };
|
|
475
|
+
export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump };
|