@fortemi/core 2026.6.4 → 2026.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aiwg-index.d.ts +9 -2
- package/dist/aiwg-index.js +72 -6
- package/dist/aiwg-index.js.map +1 -1
- package/dist/index.d.ts +1165 -681
- package/dist/index.js +2352 -1613
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -135,406 +135,63 @@ function getDataDir(persistence, archiveName) {
|
|
|
135
135
|
return void 0;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
async function createPGliteInstance(persistence, archiveName = "default") {
|
|
138
|
+
async function createPGliteInstance(persistence, archiveName = "default", options = {}) {
|
|
139
139
|
const dataDir = getDataDir(persistence, archiveName);
|
|
140
|
-
const
|
|
140
|
+
const pgliteOptions = {
|
|
141
141
|
database: "postgres",
|
|
142
142
|
// PGlite 0.4.x breaking change: explicit required
|
|
143
143
|
extensions: { vector }
|
|
144
144
|
};
|
|
145
145
|
if (dataDir) {
|
|
146
|
-
|
|
146
|
+
pgliteOptions.dataDir = dataDir;
|
|
147
147
|
}
|
|
148
|
-
|
|
148
|
+
if (options.loadDataDir) {
|
|
149
|
+
pgliteOptions.loadDataDir = options.loadDataDir;
|
|
150
|
+
}
|
|
151
|
+
const db = await PGlite.create(pgliteOptions);
|
|
149
152
|
await db.exec("CREATE EXTENSION IF NOT EXISTS vector");
|
|
150
153
|
return db;
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
// src/
|
|
154
|
-
var
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!("id" in msg)) return;
|
|
167
|
-
const pending = this.pending.get(msg.id);
|
|
168
|
-
if (!pending) return;
|
|
169
|
-
this.pending.delete(msg.id);
|
|
170
|
-
if (msg.type === "ERROR") {
|
|
171
|
-
pending.reject(new Error(msg.error));
|
|
172
|
-
} else {
|
|
173
|
-
pending.resolve(msg);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
pending = /* @__PURE__ */ new Map();
|
|
178
|
-
readyPromise;
|
|
179
|
-
resolveReady;
|
|
180
|
-
/** Resolves when the worker broadcasts READY after database initialisation. */
|
|
181
|
-
async waitReady() {
|
|
182
|
-
return this.readyPromise;
|
|
183
|
-
}
|
|
184
|
-
send(request) {
|
|
185
|
-
const id = generateId();
|
|
186
|
-
return new Promise((resolve, reject) => {
|
|
187
|
-
this.pending.set(id, {
|
|
188
|
-
resolve,
|
|
189
|
-
reject
|
|
190
|
-
});
|
|
191
|
-
this.worker.postMessage({ ...request, id });
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
async query(sql, params) {
|
|
195
|
-
const resp = await this.send({ type: "QUERY", sql, params });
|
|
196
|
-
return { rows: resp.rows, fields: resp.fields };
|
|
197
|
-
}
|
|
198
|
-
async exec(sql) {
|
|
199
|
-
await this.send({ type: "EXEC", sql });
|
|
200
|
-
}
|
|
201
|
-
async transaction(fn) {
|
|
202
|
-
const resp = await this.send({ type: "BEGIN" });
|
|
203
|
-
const txId = resp.txId;
|
|
204
|
-
const proxy = new TransactionProxy(this, txId);
|
|
205
|
-
try {
|
|
206
|
-
const result = await fn(proxy);
|
|
207
|
-
await this.send({ type: "COMMIT", txId });
|
|
208
|
-
return result;
|
|
209
|
-
} catch (err) {
|
|
210
|
-
await this.send({ type: "ROLLBACK", txId }).catch(() => {
|
|
211
|
-
});
|
|
212
|
-
throw err;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
/** Forward TX_QUERY for TransactionProxy — not part of the public surface. */
|
|
216
|
-
async _txQuery(txId, sql, params) {
|
|
217
|
-
const resp = await this.send({
|
|
218
|
-
type: "TX_QUERY",
|
|
219
|
-
txId,
|
|
220
|
-
sql,
|
|
221
|
-
params
|
|
222
|
-
});
|
|
223
|
-
return { rows: resp.rows };
|
|
224
|
-
}
|
|
225
|
-
/** Forward TX_EXEC for TransactionProxy — not part of the public surface. */
|
|
226
|
-
async _txExec(txId, sql) {
|
|
227
|
-
await this.send({ type: "TX_EXEC", txId, sql });
|
|
228
|
-
}
|
|
229
|
-
async ping() {
|
|
230
|
-
await this.send({ type: "PING" });
|
|
231
|
-
}
|
|
232
|
-
async close() {
|
|
233
|
-
await this.send({ type: "CLOSE" });
|
|
234
|
-
this.worker.terminate();
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
var TransactionProxy = class {
|
|
238
|
-
constructor(client, txId) {
|
|
239
|
-
this.client = client;
|
|
240
|
-
this.txId = txId;
|
|
241
|
-
}
|
|
242
|
-
async query(sql, params) {
|
|
243
|
-
return this.client._txQuery(this.txId, sql, params);
|
|
244
|
-
}
|
|
245
|
-
async exec(sql) {
|
|
246
|
-
return this.client._txExec(this.txId, sql);
|
|
247
|
-
}
|
|
248
|
-
};
|
|
156
|
+
// src/migrations/0001_initial_schema.ts
|
|
157
|
+
var migration0001 = {
|
|
158
|
+
version: 1,
|
|
159
|
+
name: "0001_initial_schema",
|
|
160
|
+
sql: `
|
|
161
|
+
-- Archive (multi-database tracking)
|
|
162
|
+
CREATE TABLE IF NOT EXISTS archive (
|
|
163
|
+
id TEXT PRIMARY KEY,
|
|
164
|
+
name TEXT NOT NULL UNIQUE,
|
|
165
|
+
schema_version INTEGER NOT NULL DEFAULT 0,
|
|
166
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
167
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
168
|
+
);
|
|
249
169
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return this.db.close();
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
var PGliteStorageBackendFactory = class {
|
|
271
|
-
async open(input) {
|
|
272
|
-
const db = await createPGliteInstance(input.persistence, input.archiveName);
|
|
273
|
-
return new PGliteStorageBackend(`pglite:${input.persistence}:${input.archiveName}`, db);
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
var defaultStorageBackendFactory = new PGliteStorageBackendFactory();
|
|
277
|
-
var PGliteWorkerStorageBackend = class {
|
|
278
|
-
constructor(id, client) {
|
|
279
|
-
this.id = id;
|
|
280
|
-
this.client = client;
|
|
281
|
-
}
|
|
282
|
-
mode = "readwrite";
|
|
283
|
-
query(sql, params) {
|
|
284
|
-
return this.client.query(sql, params);
|
|
285
|
-
}
|
|
286
|
-
exec(sql) {
|
|
287
|
-
return this.client.exec(sql);
|
|
288
|
-
}
|
|
289
|
-
transaction(fn) {
|
|
290
|
-
return this.client.transaction(fn);
|
|
291
|
-
}
|
|
292
|
-
close() {
|
|
293
|
-
return this.client.close();
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
var PGliteWorkerStorageBackendFactory = class {
|
|
297
|
-
constructor(options) {
|
|
298
|
-
this.options = options;
|
|
299
|
-
}
|
|
300
|
-
async open(input) {
|
|
301
|
-
const worker = this.options.createWorker();
|
|
302
|
-
const client = new PGliteWorkerClient(worker);
|
|
303
|
-
worker.postMessage({
|
|
304
|
-
type: "INIT",
|
|
305
|
-
persistence: input.persistence,
|
|
306
|
-
archiveName: input.archiveName
|
|
307
|
-
});
|
|
308
|
-
await client.waitReady();
|
|
309
|
-
return new PGliteWorkerStorageBackend(`pglite-worker:${input.persistence}:${input.archiveName}`, client);
|
|
310
|
-
}
|
|
311
|
-
};
|
|
170
|
+
-- Note (core entity)
|
|
171
|
+
CREATE TABLE IF NOT EXISTS note (
|
|
172
|
+
id TEXT PRIMARY KEY,
|
|
173
|
+
archive_id TEXT REFERENCES archive(id),
|
|
174
|
+
title TEXT,
|
|
175
|
+
format TEXT NOT NULL DEFAULT 'markdown',
|
|
176
|
+
source TEXT NOT NULL DEFAULT 'user',
|
|
177
|
+
visibility TEXT NOT NULL DEFAULT 'private',
|
|
178
|
+
revision_mode TEXT NOT NULL DEFAULT 'standard',
|
|
179
|
+
is_starred BOOLEAN NOT NULL DEFAULT false,
|
|
180
|
+
is_pinned BOOLEAN NOT NULL DEFAULT false,
|
|
181
|
+
is_archived BOOLEAN NOT NULL DEFAULT false,
|
|
182
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
183
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
184
|
+
deleted_at TIMESTAMPTZ,
|
|
185
|
+
tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, ''))) STORED
|
|
186
|
+
);
|
|
312
187
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
function assertTransition(method, name, current) {
|
|
321
|
-
const allowed = VALID_TRANSITIONS[method];
|
|
322
|
-
if (!allowed.includes(current)) {
|
|
323
|
-
throw new Error(
|
|
324
|
-
`CapabilityManager.${method}('${name}'): invalid transition from state '${current}'. Allowed source states: ${allowed.join(", ")}.`
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
var CapabilityManager = class {
|
|
329
|
-
constructor(events) {
|
|
330
|
-
this.events = events;
|
|
331
|
-
const names = ["semantic", "llm", "audio", "vision", "pdf"];
|
|
332
|
-
for (const name of names) {
|
|
333
|
-
this.capabilities.set(name, { name, state: "unloaded" });
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
capabilities = /* @__PURE__ */ new Map();
|
|
337
|
-
loaders = /* @__PURE__ */ new Map();
|
|
338
|
-
progressMessages = /* @__PURE__ */ new Map();
|
|
339
|
-
/**
|
|
340
|
-
* Register an async loader for a capability.
|
|
341
|
-
* Called by enable(); if no loader is registered the capability transitions
|
|
342
|
-
* directly to ready (useful for capabilities that require no async init).
|
|
343
|
-
*/
|
|
344
|
-
registerLoader(name, loader) {
|
|
345
|
-
this.loaders.set(name, loader);
|
|
346
|
-
}
|
|
347
|
-
getState(name) {
|
|
348
|
-
return this.capabilities.get(name)?.state ?? "unloaded";
|
|
349
|
-
}
|
|
350
|
-
isReady(name) {
|
|
351
|
-
return this.getState(name) === "ready";
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Enable a capability.
|
|
355
|
-
* Valid from: unloaded, disabled, error (retry).
|
|
356
|
-
* Runs the registered loader if present; transitions to ready on success,
|
|
357
|
-
* error on failure.
|
|
358
|
-
*/
|
|
359
|
-
async enable(name) {
|
|
360
|
-
const entry = this.capabilities.get(name);
|
|
361
|
-
if (!entry) return;
|
|
362
|
-
if (entry.state === "ready" || entry.state === "loading") return;
|
|
363
|
-
assertTransition("enable", name, entry.state);
|
|
364
|
-
entry.state = "loading";
|
|
365
|
-
entry.error = void 0;
|
|
366
|
-
this.events.emit("capability.loading", { name });
|
|
367
|
-
const loader = this.loaders.get(name);
|
|
368
|
-
if (!loader) {
|
|
369
|
-
entry.state = "ready";
|
|
370
|
-
this.events.emit("capability.ready", { name });
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
try {
|
|
374
|
-
await loader();
|
|
375
|
-
if (entry.state === "loading") {
|
|
376
|
-
entry.state = "ready";
|
|
377
|
-
this.events.emit("capability.ready", { name });
|
|
378
|
-
}
|
|
379
|
-
} catch (err) {
|
|
380
|
-
if (entry.state === "loading") {
|
|
381
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
382
|
-
entry.state = "error";
|
|
383
|
-
entry.error = message;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* Disable a ready capability.
|
|
389
|
-
* Valid from: ready only.
|
|
390
|
-
*/
|
|
391
|
-
disable(name) {
|
|
392
|
-
const entry = this.capabilities.get(name);
|
|
393
|
-
if (!entry) return;
|
|
394
|
-
assertTransition("disable", name, entry.state);
|
|
395
|
-
entry.state = "disabled";
|
|
396
|
-
this.events.emit("capability.disabled", { name });
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Mark a loading capability as ready (external use, e.g. bridge protocol).
|
|
400
|
-
* Valid from: loading only.
|
|
401
|
-
*/
|
|
402
|
-
markReady(name) {
|
|
403
|
-
const entry = this.capabilities.get(name);
|
|
404
|
-
if (!entry) return;
|
|
405
|
-
assertTransition("markReady", name, entry.state);
|
|
406
|
-
entry.state = "ready";
|
|
407
|
-
entry.error = void 0;
|
|
408
|
-
this.events.emit("capability.ready", { name });
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Mark a loading capability as errored (external use, e.g. bridge protocol).
|
|
412
|
-
* Valid from: loading only.
|
|
413
|
-
*/
|
|
414
|
-
markError(name, error) {
|
|
415
|
-
const entry = this.capabilities.get(name);
|
|
416
|
-
if (!entry) return;
|
|
417
|
-
assertTransition("markError", name, entry.state);
|
|
418
|
-
entry.state = "error";
|
|
419
|
-
entry.error = error;
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Report loading progress (0-100).
|
|
423
|
-
* Emits capability.loading with progress if the capability is currently loading.
|
|
424
|
-
* No-op if the capability is not in loading state.
|
|
425
|
-
*/
|
|
426
|
-
reportProgress(name, progress) {
|
|
427
|
-
const entry = this.capabilities.get(name);
|
|
428
|
-
if (!entry || entry.state !== "loading") return;
|
|
429
|
-
this.events.emit("capability.loading", { name, progress });
|
|
430
|
-
}
|
|
431
|
-
/** Set a human-readable progress message for a loading capability */
|
|
432
|
-
setProgress(name, message) {
|
|
433
|
-
this.progressMessages.set(name, message);
|
|
434
|
-
this.events.emit("capability.loading", { name, progress: -1 });
|
|
435
|
-
}
|
|
436
|
-
/** Get the current progress message for a capability */
|
|
437
|
-
getProgress(name) {
|
|
438
|
-
return this.progressMessages.get(name);
|
|
439
|
-
}
|
|
440
|
-
getError(name) {
|
|
441
|
-
return this.capabilities.get(name)?.error;
|
|
442
|
-
}
|
|
443
|
-
listAll() {
|
|
444
|
-
return Array.from(this.capabilities.values()).map(({ name, state }) => ({
|
|
445
|
-
name,
|
|
446
|
-
state
|
|
447
|
-
}));
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
// src/migration-runner.ts
|
|
452
|
-
var MigrationRunner = class {
|
|
453
|
-
constructor(db, events) {
|
|
454
|
-
this.db = db;
|
|
455
|
-
this.events = events;
|
|
456
|
-
}
|
|
457
|
-
async ensureSchemaTable() {
|
|
458
|
-
await this.db.exec(`
|
|
459
|
-
CREATE TABLE IF NOT EXISTS schema_version (
|
|
460
|
-
version INTEGER NOT NULL,
|
|
461
|
-
name TEXT NOT NULL,
|
|
462
|
-
applied_at TIMESTAMPTZ DEFAULT now(),
|
|
463
|
-
PRIMARY KEY (version)
|
|
464
|
-
)
|
|
465
|
-
`);
|
|
466
|
-
}
|
|
467
|
-
async getCurrentVersion() {
|
|
468
|
-
const result = await this.db.query(
|
|
469
|
-
"SELECT COALESCE(MAX(version), 0) AS version FROM schema_version"
|
|
470
|
-
);
|
|
471
|
-
return result.rows[0].version;
|
|
472
|
-
}
|
|
473
|
-
async apply(migrations) {
|
|
474
|
-
await this.ensureSchemaTable();
|
|
475
|
-
const currentVersion = await this.getCurrentVersion();
|
|
476
|
-
const pending = migrations.filter((m) => m.version > currentVersion).sort((a, b) => a.version - b.version);
|
|
477
|
-
let applied = 0;
|
|
478
|
-
for (const migration of pending) {
|
|
479
|
-
await this.db.transaction(async (tx) => {
|
|
480
|
-
await tx.exec(migration.sql);
|
|
481
|
-
await tx.query(
|
|
482
|
-
"INSERT INTO schema_version (version, name) VALUES ($1, $2)",
|
|
483
|
-
[migration.version, migration.name]
|
|
484
|
-
);
|
|
485
|
-
});
|
|
486
|
-
applied++;
|
|
487
|
-
this.events?.emit("migration.applied", { version: migration.version });
|
|
488
|
-
}
|
|
489
|
-
return applied;
|
|
490
|
-
}
|
|
491
|
-
async getAppliedMigrations() {
|
|
492
|
-
const result = await this.db.query(
|
|
493
|
-
"SELECT version, name FROM schema_version ORDER BY version"
|
|
494
|
-
);
|
|
495
|
-
return result.rows;
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
// src/migrations/0001_initial_schema.ts
|
|
500
|
-
var migration0001 = {
|
|
501
|
-
version: 1,
|
|
502
|
-
name: "0001_initial_schema",
|
|
503
|
-
sql: `
|
|
504
|
-
-- Archive (multi-database tracking)
|
|
505
|
-
CREATE TABLE IF NOT EXISTS archive (
|
|
506
|
-
id TEXT PRIMARY KEY,
|
|
507
|
-
name TEXT NOT NULL UNIQUE,
|
|
508
|
-
schema_version INTEGER NOT NULL DEFAULT 0,
|
|
509
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
510
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
511
|
-
);
|
|
512
|
-
|
|
513
|
-
-- Note (core entity)
|
|
514
|
-
CREATE TABLE IF NOT EXISTS note (
|
|
515
|
-
id TEXT PRIMARY KEY,
|
|
516
|
-
archive_id TEXT REFERENCES archive(id),
|
|
517
|
-
title TEXT,
|
|
518
|
-
format TEXT NOT NULL DEFAULT 'markdown',
|
|
519
|
-
source TEXT NOT NULL DEFAULT 'user',
|
|
520
|
-
visibility TEXT NOT NULL DEFAULT 'private',
|
|
521
|
-
revision_mode TEXT NOT NULL DEFAULT 'standard',
|
|
522
|
-
is_starred BOOLEAN NOT NULL DEFAULT false,
|
|
523
|
-
is_pinned BOOLEAN NOT NULL DEFAULT false,
|
|
524
|
-
is_archived BOOLEAN NOT NULL DEFAULT false,
|
|
525
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
526
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
527
|
-
deleted_at TIMESTAMPTZ,
|
|
528
|
-
tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, ''))) STORED
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
-- Note original content (immutable)
|
|
532
|
-
CREATE TABLE IF NOT EXISTS note_original (
|
|
533
|
-
id TEXT PRIMARY KEY,
|
|
534
|
-
note_id TEXT NOT NULL REFERENCES note(id),
|
|
535
|
-
content TEXT NOT NULL,
|
|
536
|
-
content_hash TEXT NOT NULL,
|
|
537
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
188
|
+
-- Note original content (immutable)
|
|
189
|
+
CREATE TABLE IF NOT EXISTS note_original (
|
|
190
|
+
id TEXT PRIMARY KEY,
|
|
191
|
+
note_id TEXT NOT NULL REFERENCES note(id),
|
|
192
|
+
content TEXT NOT NULL,
|
|
193
|
+
content_hash TEXT NOT NULL,
|
|
194
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
538
195
|
);
|
|
539
196
|
|
|
540
197
|
-- Note revised current (mutable, latest revision)
|
|
@@ -952,1196 +609,931 @@ var allMigrations = [
|
|
|
952
609
|
migration0009
|
|
953
610
|
];
|
|
954
611
|
|
|
955
|
-
// src/archive
|
|
956
|
-
var
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
currentArchive = "default";
|
|
972
|
-
db = null;
|
|
973
|
-
archives = /* @__PURE__ */ new Map();
|
|
974
|
-
persistence;
|
|
975
|
-
backendFactory;
|
|
976
|
-
getCurrentArchiveName() {
|
|
977
|
-
return this.currentArchive;
|
|
978
|
-
}
|
|
979
|
-
getDb() {
|
|
980
|
-
return this.db;
|
|
612
|
+
// src/data-archive.ts
|
|
613
|
+
var DB_SNAPSHOT_SCHEMA_VERSION = "fortemi.db-snapshot.v1";
|
|
614
|
+
var SUPPORTED_PGLITE_VERSION = "0.4.1";
|
|
615
|
+
var CURRENT_MIGRATION_HEAD = allMigrations.reduce(
|
|
616
|
+
(head, migration) => Math.max(head, migration.version),
|
|
617
|
+
0
|
|
618
|
+
);
|
|
619
|
+
async function readMigrationHead(db) {
|
|
620
|
+
try {
|
|
621
|
+
const result = await db.query(
|
|
622
|
+
"SELECT COALESCE(MAX(version), 0) AS head FROM schema_version"
|
|
623
|
+
);
|
|
624
|
+
return Number(result.rows[0]?.head ?? 0);
|
|
625
|
+
} catch {
|
|
626
|
+
return 0;
|
|
981
627
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
await runner.apply(allMigrations);
|
|
992
|
-
this.currentArchive = archiveName;
|
|
993
|
-
if (!this.archives.has(archiveName)) {
|
|
994
|
-
this.archives.set(archiveName, {
|
|
995
|
-
name: archiveName,
|
|
996
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
this.events?.emit("archive.switched", { name: archiveName });
|
|
1000
|
-
return this.db;
|
|
628
|
+
}
|
|
629
|
+
async function readPgvectorVersion(db) {
|
|
630
|
+
try {
|
|
631
|
+
const result = await db.query(
|
|
632
|
+
"SELECT extversion FROM pg_extension WHERE extname = 'vector'"
|
|
633
|
+
);
|
|
634
|
+
return result.rows[0]?.extversion ?? null;
|
|
635
|
+
} catch {
|
|
636
|
+
return null;
|
|
1001
637
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
638
|
+
}
|
|
639
|
+
async function dumpDbSnapshot(db, options = {}) {
|
|
640
|
+
const migration_head = await readMigrationHead(db);
|
|
641
|
+
const pgvector_version = await readPgvectorVersion(db);
|
|
642
|
+
const data = await db.dumpDataDir(options.compression ?? "gzip");
|
|
643
|
+
const meta = {
|
|
644
|
+
schema_version: DB_SNAPSHOT_SCHEMA_VERSION,
|
|
645
|
+
pglite_version: SUPPORTED_PGLITE_VERSION,
|
|
646
|
+
pgvector_version,
|
|
647
|
+
migration_head,
|
|
648
|
+
created_at: options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
649
|
+
...options.fortemiVersion ? { fortemi_version: options.fortemiVersion } : {}
|
|
650
|
+
};
|
|
651
|
+
return { data, meta };
|
|
652
|
+
}
|
|
653
|
+
function majorMinor(version) {
|
|
654
|
+
const parts = version.split(".");
|
|
655
|
+
return `${parts[0] ?? "0"}.${parts[1] ?? "0"}`;
|
|
656
|
+
}
|
|
657
|
+
function verifyDbSnapshotMeta(meta, expected = {}) {
|
|
658
|
+
const reasons = [];
|
|
659
|
+
const warnings = [];
|
|
660
|
+
const expectedHead = expected.migrationHead ?? CURRENT_MIGRATION_HEAD;
|
|
661
|
+
const expectedPglite = expected.pgliteVersion ?? SUPPORTED_PGLITE_VERSION;
|
|
662
|
+
if (meta.schema_version !== DB_SNAPSHOT_SCHEMA_VERSION) {
|
|
663
|
+
reasons.push(`unsupported snapshot schema_version: ${String(meta.schema_version)}`);
|
|
1007
664
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
665
|
+
if (meta.migration_head !== expectedHead) {
|
|
666
|
+
reasons.push(`migration head mismatch: snapshot=${meta.migration_head}, supported=${expectedHead}`);
|
|
1010
667
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
throw new Error("Cannot delete the default archive");
|
|
1014
|
-
}
|
|
1015
|
-
if (this.currentArchive === archiveName && this.db) {
|
|
1016
|
-
await this.db.close();
|
|
1017
|
-
this.db = null;
|
|
1018
|
-
}
|
|
1019
|
-
this.archives.delete(archiveName);
|
|
668
|
+
if (majorMinor(meta.pglite_version) !== majorMinor(expectedPglite)) {
|
|
669
|
+
reasons.push(`PGlite version mismatch: snapshot=${meta.pglite_version}, supported=${expectedPglite}`);
|
|
1020
670
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
671
|
+
if (expected.pgvectorVersion !== void 0 && meta.pgvector_version !== expected.pgvectorVersion) {
|
|
672
|
+
warnings.push(`pgvector version differs: snapshot=${String(meta.pgvector_version)}, expected=${String(expected.pgvectorVersion)}`);
|
|
1023
673
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
674
|
+
return { compatible: reasons.length === 0, reasons, warnings };
|
|
675
|
+
}
|
|
676
|
+
var DbSnapshotVersionError = class extends Error {
|
|
677
|
+
reasons;
|
|
678
|
+
meta;
|
|
679
|
+
constructor(reasons, meta) {
|
|
680
|
+
super(`Incompatible DB snapshot: ${reasons.join("; ")}`);
|
|
681
|
+
this.name = "DbSnapshotVersionError";
|
|
682
|
+
this.reasons = reasons;
|
|
683
|
+
this.meta = meta;
|
|
1029
684
|
}
|
|
1030
685
|
};
|
|
686
|
+
function isInlineSnapshot(source) {
|
|
687
|
+
return typeof source === "object" && "data" in source && "meta" in source;
|
|
688
|
+
}
|
|
689
|
+
async function resolveSnapshotSource(source, fetchImpl) {
|
|
690
|
+
if (isInlineSnapshot(source)) return source;
|
|
691
|
+
const dataUrl = typeof source === "string" ? source : source.dataUrl;
|
|
692
|
+
const metaUrl = typeof source === "string" ? `${source}.meta.json` : source.metaUrl ?? `${source.dataUrl}.meta.json`;
|
|
693
|
+
const metaResponse = await fetchImpl(metaUrl);
|
|
694
|
+
if (!metaResponse.ok) {
|
|
695
|
+
throw new Error(`Failed to fetch snapshot meta (${metaResponse.status}): ${metaUrl}`);
|
|
696
|
+
}
|
|
697
|
+
const meta = await metaResponse.json();
|
|
698
|
+
const dataResponse = await fetchImpl(dataUrl);
|
|
699
|
+
if (!dataResponse.ok) {
|
|
700
|
+
throw new Error(`Failed to fetch snapshot data (${dataResponse.status}): ${dataUrl}`);
|
|
701
|
+
}
|
|
702
|
+
const data = await dataResponse.blob();
|
|
703
|
+
return { data, meta };
|
|
704
|
+
}
|
|
705
|
+
async function restoreDbSnapshot(source, options = {}) {
|
|
706
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
707
|
+
const { data, meta } = await resolveSnapshotSource(source, fetchImpl);
|
|
708
|
+
const compat = verifyDbSnapshotMeta(meta, options.expectations);
|
|
709
|
+
if (!compat.compatible) {
|
|
710
|
+
throw new DbSnapshotVersionError(compat.reasons, meta);
|
|
711
|
+
}
|
|
712
|
+
const createOptions = { loadDataDir: data };
|
|
713
|
+
return createPGliteInstance(options.persistence ?? "memory", options.archiveName ?? "default", createOptions);
|
|
714
|
+
}
|
|
715
|
+
function computeHash(data) {
|
|
716
|
+
const digest = sha256(data);
|
|
717
|
+
return `sha256:${bytesToHex(digest)}`;
|
|
718
|
+
}
|
|
1031
719
|
|
|
1032
|
-
// src/
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
events
|
|
1037
|
-
config,
|
|
1038
|
-
destroy() {
|
|
1039
|
-
events.removeAllListeners();
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
function computeHash(data) {
|
|
1044
|
-
const digest = sha256(data);
|
|
1045
|
-
return `sha256:${bytesToHex(digest)}`;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
// src/service-worker/register.ts
|
|
1049
|
-
async function registerServiceWorker(swUrl = "/sw.js") {
|
|
1050
|
-
if (!("serviceWorker" in navigator)) {
|
|
1051
|
-
return { registered: false, error: "Service Workers not supported" };
|
|
720
|
+
// src/repositories/notes-repository.ts
|
|
721
|
+
var NotesRepository = class {
|
|
722
|
+
constructor(db, events) {
|
|
723
|
+
this.db = db;
|
|
724
|
+
this.events = events;
|
|
1052
725
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
726
|
+
/**
|
|
727
|
+
* Create a new note with its original content record, current revision,
|
|
728
|
+
* optional tags, and an auto-queued title_generation job when no title
|
|
729
|
+
* is provided.
|
|
730
|
+
*
|
|
731
|
+
* All writes happen in a single transaction.
|
|
732
|
+
*/
|
|
733
|
+
async create(input) {
|
|
734
|
+
const noteId = generateId();
|
|
735
|
+
const originalId = generateId();
|
|
736
|
+
const contentHash = computeHash(new TextEncoder().encode(input.content));
|
|
737
|
+
await this.db.transaction(async (tx) => {
|
|
738
|
+
await tx.query(
|
|
739
|
+
`INSERT INTO note (id, archive_id, title, format, source, visibility)
|
|
740
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
741
|
+
[
|
|
742
|
+
noteId,
|
|
743
|
+
input.archive_id ?? null,
|
|
744
|
+
input.title ?? null,
|
|
745
|
+
input.format ?? "markdown",
|
|
746
|
+
input.source ?? "user",
|
|
747
|
+
input.visibility ?? "private"
|
|
748
|
+
]
|
|
749
|
+
);
|
|
750
|
+
await tx.query(
|
|
751
|
+
`INSERT INTO note_original (id, note_id, content, content_hash)
|
|
752
|
+
VALUES ($1, $2, $3, $4)`,
|
|
753
|
+
[originalId, noteId, input.content, contentHash]
|
|
754
|
+
);
|
|
755
|
+
await tx.query(
|
|
756
|
+
`INSERT INTO note_revised_current (note_id, content)
|
|
757
|
+
VALUES ($1, $2)`,
|
|
758
|
+
[noteId, input.content]
|
|
759
|
+
);
|
|
760
|
+
if (input.tags?.length) {
|
|
761
|
+
for (const tag of input.tags) {
|
|
762
|
+
await tx.query(
|
|
763
|
+
`INSERT INTO note_tag (id, note_id, tag) VALUES ($1, $2, $3)`,
|
|
764
|
+
[generateId(), noteId, tag]
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
await tx.query(
|
|
769
|
+
`INSERT INTO job_queue (id, note_id, job_type, status, priority, required_capability)
|
|
770
|
+
VALUES ($1, $2, 'ai_revision', 'pending', 1, 'llm')`,
|
|
771
|
+
[generateId(), noteId]
|
|
772
|
+
);
|
|
773
|
+
if (!input.title) {
|
|
774
|
+
await tx.query(
|
|
775
|
+
`INSERT INTO job_queue (id, note_id, job_type, status, priority)
|
|
776
|
+
VALUES ($1, $2, 'title_generation', 'pending', 2)`,
|
|
777
|
+
[generateId(), noteId]
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
await tx.query(
|
|
781
|
+
`INSERT INTO job_queue (id, note_id, job_type, status, priority, required_capability)
|
|
782
|
+
VALUES ($1, $2, 'embedding', 'pending', 3, 'semantic')`,
|
|
783
|
+
[generateId(), noteId]
|
|
784
|
+
);
|
|
1057
785
|
});
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
786
|
+
this.events?.emit("note.created", { id: noteId });
|
|
787
|
+
return this.get(noteId);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Fetch a single note by its ID.
|
|
791
|
+
* Returns NoteFull which includes original content, current revision, and tags.
|
|
792
|
+
* Throws when the note does not exist.
|
|
793
|
+
*/
|
|
794
|
+
async get(id) {
|
|
795
|
+
const noteResult = await this.db.query(
|
|
796
|
+
`SELECT n.id, n.archive_id, n.title, n.format, n.source, n.visibility,
|
|
797
|
+
n.revision_mode, n.is_starred, n.is_pinned, n.is_archived,
|
|
798
|
+
n.created_at, n.updated_at, n.deleted_at,
|
|
799
|
+
o.id AS original_id,
|
|
800
|
+
o.content AS original_content,
|
|
801
|
+
o.content_hash,
|
|
802
|
+
o.created_at AS original_created_at,
|
|
803
|
+
c.content AS current_content,
|
|
804
|
+
c.ai_metadata,
|
|
805
|
+
c.generation_count,
|
|
806
|
+
c.model,
|
|
807
|
+
c.is_user_edited,
|
|
808
|
+
c.updated_at AS current_updated_at
|
|
809
|
+
FROM note n
|
|
810
|
+
LEFT JOIN note_original o ON o.note_id = n.id
|
|
811
|
+
LEFT JOIN note_revised_current c ON c.note_id = n.id
|
|
812
|
+
WHERE n.id = $1`,
|
|
813
|
+
[id]
|
|
814
|
+
);
|
|
815
|
+
if (noteResult.rows.length === 0) {
|
|
816
|
+
throw new Error(`Note not found: ${id}`);
|
|
1067
817
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
818
|
+
const row = noteResult.rows[0];
|
|
819
|
+
const tagsResult = await this.db.query(
|
|
820
|
+
`SELECT tag FROM note_tag WHERE note_id = $1 ORDER BY tag`,
|
|
821
|
+
[id]
|
|
822
|
+
);
|
|
1070
823
|
return {
|
|
1071
|
-
|
|
1072
|
-
|
|
824
|
+
id: row.id,
|
|
825
|
+
archive_id: row.archive_id,
|
|
826
|
+
title: row.title,
|
|
827
|
+
format: row.format,
|
|
828
|
+
source: row.source,
|
|
829
|
+
visibility: row.visibility,
|
|
830
|
+
revision_mode: row.revision_mode,
|
|
831
|
+
is_starred: row.is_starred,
|
|
832
|
+
is_pinned: row.is_pinned,
|
|
833
|
+
is_archived: row.is_archived,
|
|
834
|
+
created_at: row.created_at,
|
|
835
|
+
updated_at: row.updated_at,
|
|
836
|
+
deleted_at: row.deleted_at,
|
|
837
|
+
tags: tagsResult.rows.map((r) => r.tag),
|
|
838
|
+
original: {
|
|
839
|
+
id: row.original_id,
|
|
840
|
+
content: row.original_content,
|
|
841
|
+
content_hash: row.content_hash,
|
|
842
|
+
created_at: row.original_created_at
|
|
843
|
+
},
|
|
844
|
+
current: {
|
|
845
|
+
content: row.current_content,
|
|
846
|
+
ai_metadata: row.ai_metadata,
|
|
847
|
+
generation_count: row.generation_count,
|
|
848
|
+
model: row.model,
|
|
849
|
+
is_user_edited: row.is_user_edited,
|
|
850
|
+
updated_at: row.current_updated_at
|
|
851
|
+
}
|
|
1073
852
|
};
|
|
1074
853
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
{
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
{
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
{
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
1153
|
-
}
|
|
1154
|
-
},
|
|
1155
|
-
// DELETE /api/v1/notes/:id — soft-delete a note
|
|
1156
|
-
{
|
|
1157
|
-
method: "DELETE",
|
|
1158
|
-
pattern: /^\/api\/v1\/notes\/([^/]+)\/?$/,
|
|
1159
|
-
handler: async () => {
|
|
1160
|
-
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
1161
|
-
}
|
|
1162
|
-
},
|
|
1163
|
-
// GET /api/v1/search — full-text search
|
|
1164
|
-
{
|
|
1165
|
-
method: "GET",
|
|
1166
|
-
pattern: /^\/api\/v1\/search\/?$/,
|
|
1167
|
-
handler: async () => {
|
|
1168
|
-
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
854
|
+
/**
|
|
855
|
+
* List notes with optional filtering, sorting, and pagination.
|
|
856
|
+
* Excludes soft-deleted notes by default (pass include_deleted: true to override).
|
|
857
|
+
*/
|
|
858
|
+
async list(options = {}) {
|
|
859
|
+
const {
|
|
860
|
+
limit = 50,
|
|
861
|
+
offset = 0,
|
|
862
|
+
sort = "created_at",
|
|
863
|
+
order = "desc",
|
|
864
|
+
is_starred,
|
|
865
|
+
is_pinned,
|
|
866
|
+
is_archived,
|
|
867
|
+
include_deleted = false,
|
|
868
|
+
collection_id,
|
|
869
|
+
tags
|
|
870
|
+
} = options;
|
|
871
|
+
const conditions = [];
|
|
872
|
+
const params = [];
|
|
873
|
+
let paramIndex = 1;
|
|
874
|
+
if (!include_deleted) {
|
|
875
|
+
conditions.push("n.deleted_at IS NULL");
|
|
876
|
+
}
|
|
877
|
+
if (is_starred !== void 0) {
|
|
878
|
+
conditions.push(`n.is_starred = $${paramIndex++}`);
|
|
879
|
+
params.push(is_starred);
|
|
880
|
+
}
|
|
881
|
+
if (is_pinned !== void 0) {
|
|
882
|
+
conditions.push(`n.is_pinned = $${paramIndex++}`);
|
|
883
|
+
params.push(is_pinned);
|
|
884
|
+
}
|
|
885
|
+
if (is_archived !== void 0) {
|
|
886
|
+
conditions.push(`n.is_archived = $${paramIndex++}`);
|
|
887
|
+
params.push(is_archived);
|
|
888
|
+
}
|
|
889
|
+
if (collection_id) {
|
|
890
|
+
conditions.push(
|
|
891
|
+
`EXISTS (SELECT 1 FROM collection_note cn WHERE cn.note_id = n.id AND cn.collection_id = $${paramIndex++})`
|
|
892
|
+
);
|
|
893
|
+
params.push(collection_id);
|
|
894
|
+
}
|
|
895
|
+
if (tags?.length) {
|
|
896
|
+
conditions.push(
|
|
897
|
+
`EXISTS (SELECT 1 FROM note_tag nt WHERE nt.note_id = n.id AND nt.tag = ANY($${paramIndex++}))`
|
|
898
|
+
);
|
|
899
|
+
params.push(tags);
|
|
900
|
+
}
|
|
901
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
902
|
+
const validSorts = ["created_at", "updated_at", "title"];
|
|
903
|
+
const sortCol = validSorts.includes(sort) ? sort : "created_at";
|
|
904
|
+
const sortDir = order === "asc" ? "ASC" : "DESC";
|
|
905
|
+
const countResult = await this.db.query(
|
|
906
|
+
`SELECT COUNT(*) AS count FROM note n ${where}`,
|
|
907
|
+
params
|
|
908
|
+
);
|
|
909
|
+
const total = parseInt(countResult.rows[0].count, 10);
|
|
910
|
+
const listParams = [...params, limit, offset];
|
|
911
|
+
const rowsResult = await this.db.query(
|
|
912
|
+
`SELECT n.id, n.title, n.format, n.source, n.visibility,
|
|
913
|
+
n.is_starred, n.is_pinned, n.is_archived,
|
|
914
|
+
n.created_at, n.updated_at, n.deleted_at
|
|
915
|
+
FROM note n ${where}
|
|
916
|
+
ORDER BY n.${sortCol} ${sortDir}
|
|
917
|
+
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
|
918
|
+
listParams
|
|
919
|
+
);
|
|
920
|
+
const noteIds = rowsResult.rows.map((r) => r.id);
|
|
921
|
+
const tagMap = /* @__PURE__ */ new Map();
|
|
922
|
+
if (noteIds.length > 0) {
|
|
923
|
+
const tagsResult = await this.db.query(
|
|
924
|
+
`SELECT note_id, tag FROM note_tag WHERE note_id = ANY($1) ORDER BY tag`,
|
|
925
|
+
[noteIds]
|
|
926
|
+
);
|
|
927
|
+
for (const row of tagsResult.rows) {
|
|
928
|
+
const existing = tagMap.get(row.note_id) ?? [];
|
|
929
|
+
existing.push(row.tag);
|
|
930
|
+
tagMap.set(row.note_id, existing);
|
|
1169
931
|
}
|
|
1170
932
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
dir2: hash.slice(2, 4),
|
|
1187
|
-
filename: hash
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
var OpfsBlobStore = class {
|
|
1191
|
-
constructor(archiveName) {
|
|
1192
|
-
this.archiveName = archiveName;
|
|
1193
|
-
}
|
|
1194
|
-
async getRoot() {
|
|
1195
|
-
const root = await navigator.storage.getDirectory();
|
|
1196
|
-
return root.getDirectoryHandle(`fortemi-${this.archiveName}-blobs`, { create: true });
|
|
933
|
+
const items = rowsResult.rows.map((r) => ({
|
|
934
|
+
id: r.id,
|
|
935
|
+
title: r.title,
|
|
936
|
+
format: r.format,
|
|
937
|
+
source: r.source,
|
|
938
|
+
visibility: r.visibility,
|
|
939
|
+
is_starred: r.is_starred,
|
|
940
|
+
is_pinned: r.is_pinned,
|
|
941
|
+
is_archived: r.is_archived,
|
|
942
|
+
created_at: r.created_at,
|
|
943
|
+
updated_at: r.updated_at,
|
|
944
|
+
deleted_at: r.deleted_at,
|
|
945
|
+
tags: tagMap.get(r.id) ?? []
|
|
946
|
+
}));
|
|
947
|
+
return { items, total, limit, offset };
|
|
1197
948
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
949
|
+
/**
|
|
950
|
+
* Update mutable note fields.
|
|
951
|
+
* When content changes, the previous current content is saved as a numbered
|
|
952
|
+
* revision before the new content is applied.
|
|
953
|
+
*/
|
|
954
|
+
async update(id, input) {
|
|
955
|
+
const countResult = await this.db.query(
|
|
956
|
+
`SELECT COUNT(*) AS count FROM note_revision WHERE note_id = $1`,
|
|
957
|
+
[id]
|
|
958
|
+
);
|
|
959
|
+
const nextRevision = parseInt(countResult.rows[0].count, 10) + 1;
|
|
960
|
+
await this.db.transaction(async (tx) => {
|
|
961
|
+
const setClauses = ["updated_at = now()"];
|
|
962
|
+
const noteParams = [];
|
|
963
|
+
let paramIdx = 1;
|
|
964
|
+
if (input.title !== void 0) {
|
|
965
|
+
setClauses.push(`title = $${paramIdx++}`);
|
|
966
|
+
noteParams.push(input.title);
|
|
1208
967
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
968
|
+
if (input.format !== void 0) {
|
|
969
|
+
setClauses.push(`format = $${paramIdx++}`);
|
|
970
|
+
noteParams.push(input.format);
|
|
971
|
+
}
|
|
972
|
+
if (input.visibility !== void 0) {
|
|
973
|
+
setClauses.push(`visibility = $${paramIdx++}`);
|
|
974
|
+
noteParams.push(input.visibility);
|
|
975
|
+
}
|
|
976
|
+
noteParams.push(id);
|
|
977
|
+
await tx.query(
|
|
978
|
+
`UPDATE note SET ${setClauses.join(", ")} WHERE id = $${paramIdx}`,
|
|
979
|
+
noteParams
|
|
980
|
+
);
|
|
981
|
+
if (input.content !== void 0) {
|
|
982
|
+
const currentResult = await tx.query(
|
|
983
|
+
`SELECT content FROM note_revised_current WHERE note_id = $1`,
|
|
984
|
+
[id]
|
|
985
|
+
);
|
|
986
|
+
if (currentResult.rows.length > 0) {
|
|
987
|
+
await tx.query(
|
|
988
|
+
`INSERT INTO note_revision (id, note_id, revision_number, type, content)
|
|
989
|
+
VALUES ($1, $2, $3, 'user', $4)`,
|
|
990
|
+
[generateId(), id, nextRevision, currentResult.rows[0].content]
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
await tx.query(
|
|
994
|
+
`UPDATE note_revised_current
|
|
995
|
+
SET content = $1, is_user_edited = true, updated_at = now()
|
|
996
|
+
WHERE note_id = $2`,
|
|
997
|
+
[input.content, id]
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
this.events?.emit("note.updated", { id });
|
|
1002
|
+
return this.get(id);
|
|
1211
1003
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
await
|
|
1217
|
-
|
|
1004
|
+
/**
|
|
1005
|
+
* Soft-delete a note by setting deleted_at to the current timestamp.
|
|
1006
|
+
*/
|
|
1007
|
+
async delete(id) {
|
|
1008
|
+
await this.db.query(
|
|
1009
|
+
`UPDATE note SET deleted_at = now(), updated_at = now() WHERE id = $1`,
|
|
1010
|
+
[id]
|
|
1011
|
+
);
|
|
1012
|
+
this.events?.emit("note.deleted", { id });
|
|
1218
1013
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1014
|
+
/**
|
|
1015
|
+
* Restore a soft-deleted note by clearing deleted_at.
|
|
1016
|
+
*/
|
|
1017
|
+
async restore(id) {
|
|
1018
|
+
await this.db.query(
|
|
1019
|
+
`UPDATE note SET deleted_at = NULL, updated_at = now() WHERE id = $1`,
|
|
1020
|
+
[id]
|
|
1021
|
+
);
|
|
1022
|
+
this.events?.emit("note.restored", { id });
|
|
1023
|
+
return this.get(id);
|
|
1225
1024
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
throw err;
|
|
1238
|
-
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Toggle the is_starred field on a note.
|
|
1027
|
+
*/
|
|
1028
|
+
async star(id, starred) {
|
|
1029
|
+
await this.db.query(
|
|
1030
|
+
`UPDATE note SET is_starred = $1, updated_at = now() WHERE id = $2`,
|
|
1031
|
+
[starred, id]
|
|
1032
|
+
);
|
|
1033
|
+
this.events?.emit("note.updated", { id });
|
|
1239
1034
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1035
|
+
/**
|
|
1036
|
+
* Toggle the is_pinned field on a note.
|
|
1037
|
+
*/
|
|
1038
|
+
async pin(id, pinned) {
|
|
1039
|
+
await this.db.query(
|
|
1040
|
+
`UPDATE note SET is_pinned = $1, updated_at = now() WHERE id = $2`,
|
|
1041
|
+
[pinned, id]
|
|
1042
|
+
);
|
|
1043
|
+
this.events?.emit("note.updated", { id });
|
|
1243
1044
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
};
|
|
1253
|
-
req.onsuccess = () => resolve(req.result);
|
|
1254
|
-
req.onerror = () => reject(req.error);
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1257
|
-
var IdbBlobStore = class {
|
|
1258
|
-
dbName;
|
|
1259
|
-
_db = null;
|
|
1260
|
-
constructor(archiveName) {
|
|
1261
|
-
this.dbName = `fortemi-${archiveName}-blobs`;
|
|
1045
|
+
/**
|
|
1046
|
+
* Toggle the is_archived field on a note.
|
|
1047
|
+
*/
|
|
1048
|
+
async archive(id, archived) {
|
|
1049
|
+
await this.db.query(
|
|
1050
|
+
`UPDATE note SET is_archived = $1, updated_at = now() WHERE id = $2`,
|
|
1051
|
+
[archived, id]
|
|
1052
|
+
);
|
|
1053
|
+
this.events?.emit("note.updated", { id });
|
|
1262
1054
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1055
|
+
/**
|
|
1056
|
+
* Get revision history for a note, ordered by revision_number descending.
|
|
1057
|
+
*/
|
|
1058
|
+
async getRevisions(noteId) {
|
|
1059
|
+
const result = await this.db.query(
|
|
1060
|
+
`SELECT id, note_id, revision_number, type, content, ai_metadata, model, created_at
|
|
1061
|
+
FROM note_revision
|
|
1062
|
+
WHERE note_id = $1
|
|
1063
|
+
ORDER BY revision_number DESC`,
|
|
1064
|
+
[noteId]
|
|
1065
|
+
);
|
|
1066
|
+
return result.rows;
|
|
1268
1067
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
// src/repositories/condition-builder.ts
|
|
1071
|
+
function buildNoteConditions(options, startIdx, includeDeleted = false) {
|
|
1072
|
+
const conditions = [];
|
|
1073
|
+
const params = [];
|
|
1074
|
+
let idx = startIdx;
|
|
1075
|
+
if (!includeDeleted) {
|
|
1076
|
+
conditions.push("n.deleted_at IS NULL");
|
|
1277
1077
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
req.onsuccess = () => resolve(req.result ?? null);
|
|
1284
|
-
req.onerror = () => reject(req.error);
|
|
1285
|
-
});
|
|
1078
|
+
if (options.tags?.length) {
|
|
1079
|
+
conditions.push(
|
|
1080
|
+
`EXISTS (SELECT 1 FROM note_tag nt WHERE nt.note_id = n.id AND nt.tag = ANY($${idx++}))`
|
|
1081
|
+
);
|
|
1082
|
+
params.push(options.tags);
|
|
1286
1083
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
tx.oncomplete = () => resolve();
|
|
1293
|
-
tx.onerror = () => reject(tx.error);
|
|
1294
|
-
});
|
|
1084
|
+
if (options.collection_id) {
|
|
1085
|
+
conditions.push(
|
|
1086
|
+
`EXISTS (SELECT 1 FROM collection_note cn WHERE cn.note_id = n.id AND cn.collection_id = $${idx++})`
|
|
1087
|
+
);
|
|
1088
|
+
params.push(options.collection_id);
|
|
1295
1089
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
const tx = db.transaction(IDB_STORE, "readonly");
|
|
1300
|
-
const req = tx.objectStore(IDB_STORE).count(hash);
|
|
1301
|
-
req.onsuccess = () => resolve(req.result > 0);
|
|
1302
|
-
req.onerror = () => reject(req.error);
|
|
1303
|
-
});
|
|
1090
|
+
if (options.date_from) {
|
|
1091
|
+
conditions.push(`n.created_at >= $${idx++}`);
|
|
1092
|
+
params.push(options.date_from);
|
|
1304
1093
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
async write(hash, data) {
|
|
1309
|
-
this.store.set(hash, data);
|
|
1094
|
+
if (options.date_to) {
|
|
1095
|
+
conditions.push(`n.created_at <= $${idx++}`);
|
|
1096
|
+
params.push(options.date_to);
|
|
1310
1097
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1098
|
+
if (options.is_starred !== void 0) {
|
|
1099
|
+
conditions.push(`n.is_starred = $${idx++}`);
|
|
1100
|
+
params.push(options.is_starred);
|
|
1313
1101
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1102
|
+
if (options.is_archived !== void 0) {
|
|
1103
|
+
conditions.push(`n.is_archived = $${idx++}`);
|
|
1104
|
+
params.push(options.is_archived);
|
|
1316
1105
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1106
|
+
if (options.format) {
|
|
1107
|
+
conditions.push(`n.format = $${idx++}`);
|
|
1108
|
+
params.push(options.format);
|
|
1319
1109
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
return new OpfsBlobStore(archiveName);
|
|
1110
|
+
if (options.source) {
|
|
1111
|
+
conditions.push(`n.source = $${idx++}`);
|
|
1112
|
+
params.push(options.source);
|
|
1324
1113
|
}
|
|
1325
|
-
|
|
1114
|
+
if (options.visibility) {
|
|
1115
|
+
conditions.push(`n.visibility = $${idx++}`);
|
|
1116
|
+
params.push(options.visibility);
|
|
1117
|
+
}
|
|
1118
|
+
return { conditions, params, nextIdx: idx };
|
|
1326
1119
|
}
|
|
1327
1120
|
|
|
1328
|
-
// src/repositories/
|
|
1329
|
-
var
|
|
1330
|
-
|
|
1121
|
+
// src/repositories/embedding-sets-repository.ts
|
|
1122
|
+
var DEFAULT_COMPATIBILITY = {
|
|
1123
|
+
model: "require-same",
|
|
1124
|
+
dimension: "require-same",
|
|
1125
|
+
duplicateVectors: "prefer-set-order",
|
|
1126
|
+
missingVectors: "omit"
|
|
1127
|
+
};
|
|
1128
|
+
function jsonParam(value) {
|
|
1129
|
+
return value == null ? null : JSON.stringify(value);
|
|
1130
|
+
}
|
|
1131
|
+
function asObject(value) {
|
|
1132
|
+
if (value == null) return null;
|
|
1133
|
+
if (typeof value === "string") return JSON.parse(value);
|
|
1134
|
+
return value;
|
|
1135
|
+
}
|
|
1136
|
+
function dateString(value) {
|
|
1137
|
+
if (!value) return void 0;
|
|
1138
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
1139
|
+
}
|
|
1140
|
+
function dateMillis(value) {
|
|
1141
|
+
return value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
1142
|
+
}
|
|
1143
|
+
function hashJson(value) {
|
|
1144
|
+
return computeHash(new TextEncoder().encode(JSON.stringify(value)));
|
|
1145
|
+
}
|
|
1146
|
+
var EmbeddingSetsRepository = class {
|
|
1147
|
+
constructor(db) {
|
|
1331
1148
|
this.db = db;
|
|
1332
|
-
this.events = events;
|
|
1333
1149
|
}
|
|
1334
|
-
/**
|
|
1335
|
-
* Create a new note with its original content record, current revision,
|
|
1336
|
-
* optional tags, and an auto-queued title_generation job when no title
|
|
1337
|
-
* is provided.
|
|
1338
|
-
*
|
|
1339
|
-
* All writes happen in a single transaction.
|
|
1340
|
-
*/
|
|
1341
1150
|
async create(input) {
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
[originalId, noteId, input.content, contentHash]
|
|
1362
|
-
);
|
|
1363
|
-
await tx.query(
|
|
1364
|
-
`INSERT INTO note_revised_current (note_id, content)
|
|
1365
|
-
VALUES ($1, $2)`,
|
|
1366
|
-
[noteId, input.content]
|
|
1367
|
-
);
|
|
1368
|
-
if (input.tags?.length) {
|
|
1369
|
-
for (const tag of input.tags) {
|
|
1370
|
-
await tx.query(
|
|
1371
|
-
`INSERT INTO note_tag (id, note_id, tag) VALUES ($1, $2, $3)`,
|
|
1372
|
-
[generateId(), noteId, tag]
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
await tx.query(
|
|
1377
|
-
`INSERT INTO job_queue (id, note_id, job_type, status, priority, required_capability)
|
|
1378
|
-
VALUES ($1, $2, 'ai_revision', 'pending', 1, 'llm')`,
|
|
1379
|
-
[generateId(), noteId]
|
|
1380
|
-
);
|
|
1381
|
-
if (!input.title) {
|
|
1382
|
-
await tx.query(
|
|
1383
|
-
`INSERT INTO job_queue (id, note_id, job_type, status, priority)
|
|
1384
|
-
VALUES ($1, $2, 'title_generation', 'pending', 2)`,
|
|
1385
|
-
[generateId(), noteId]
|
|
1386
|
-
);
|
|
1387
|
-
}
|
|
1388
|
-
await tx.query(
|
|
1389
|
-
`INSERT INTO job_queue (id, note_id, job_type, status, priority, required_capability)
|
|
1390
|
-
VALUES ($1, $2, 'embedding', 'pending', 3, 'semantic')`,
|
|
1391
|
-
[generateId(), noteId]
|
|
1392
|
-
);
|
|
1393
|
-
});
|
|
1394
|
-
this.events?.emit("note.created", { id: noteId });
|
|
1395
|
-
return this.get(noteId);
|
|
1151
|
+
const id = input.id ?? generateId();
|
|
1152
|
+
await this.db.query(
|
|
1153
|
+
`INSERT INTO embedding_set (
|
|
1154
|
+
id, name, purpose, model_name, dimensions, kind, mode,
|
|
1155
|
+
truncate_dimension, criteria_json
|
|
1156
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)`,
|
|
1157
|
+
[
|
|
1158
|
+
id,
|
|
1159
|
+
input.name,
|
|
1160
|
+
input.purpose ?? null,
|
|
1161
|
+
input.model_name ?? "all-MiniLM-L6-v2",
|
|
1162
|
+
input.dimensions ?? 384,
|
|
1163
|
+
input.kind ?? "physical",
|
|
1164
|
+
input.mode ?? null,
|
|
1165
|
+
input.truncate_dimension ?? null,
|
|
1166
|
+
jsonParam(input.criteria ?? null)
|
|
1167
|
+
]
|
|
1168
|
+
);
|
|
1169
|
+
return this.get(id);
|
|
1396
1170
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
LEFT JOIN note_original o ON o.note_id = n.id
|
|
1419
|
-
LEFT JOIN note_revised_current c ON c.note_id = n.id
|
|
1420
|
-
WHERE n.id = $1`,
|
|
1421
|
-
[id]
|
|
1171
|
+
async createVirtualDefinition(input) {
|
|
1172
|
+
const id = input.id ?? generateId();
|
|
1173
|
+
await this.db.query(
|
|
1174
|
+
`INSERT INTO embedding_set (
|
|
1175
|
+
id, name, purpose, model_name, dimensions, kind, mode,
|
|
1176
|
+
source_json, compatibility_json, materialization_json, freshness_json, created_at, updated_at
|
|
1177
|
+
) VALUES ($1, $2, $3, $4, $5, 'virtual', 'auto', $6::jsonb, $7::jsonb, $8::jsonb, $9::jsonb,
|
|
1178
|
+
COALESCE($10::timestamptz, now()), COALESCE($11::timestamptz, now()))`,
|
|
1179
|
+
[
|
|
1180
|
+
id,
|
|
1181
|
+
input.name,
|
|
1182
|
+
input.purpose ?? null,
|
|
1183
|
+
this.inferDefinitionModel(input) ?? "virtual",
|
|
1184
|
+
this.inferDefinitionDimension(input) ?? 0,
|
|
1185
|
+
jsonParam(input.source),
|
|
1186
|
+
jsonParam(input.compatibility),
|
|
1187
|
+
jsonParam(input.materialization ?? null),
|
|
1188
|
+
jsonParam({ status: input.materialization?.freshness ?? "unknown" }),
|
|
1189
|
+
input.createdAt ?? null,
|
|
1190
|
+
input.updatedAt ?? null
|
|
1191
|
+
]
|
|
1422
1192
|
);
|
|
1423
|
-
|
|
1424
|
-
|
|
1193
|
+
const row = await this.get(id);
|
|
1194
|
+
if (input.materialization?.allowed) {
|
|
1195
|
+
await this.refreshMaterializedVirtualSet(id);
|
|
1196
|
+
return this.get(id);
|
|
1425
1197
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1198
|
+
return row;
|
|
1199
|
+
}
|
|
1200
|
+
async ensureDefault() {
|
|
1201
|
+
const existing = await this.db.query(
|
|
1202
|
+
`SELECT * FROM embedding_set WHERE name = $1 AND model_name = $2 AND kind = 'physical' ORDER BY created_at LIMIT 1`,
|
|
1203
|
+
["Full content", "all-MiniLM-L6-v2"]
|
|
1204
|
+
);
|
|
1205
|
+
if (existing.rows.length > 0) return existing.rows[0];
|
|
1206
|
+
return this.create({
|
|
1207
|
+
name: "Full content",
|
|
1208
|
+
purpose: "Semantic search over full revised note content",
|
|
1209
|
+
model_name: "all-MiniLM-L6-v2",
|
|
1210
|
+
dimensions: 384,
|
|
1211
|
+
kind: "physical"
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
async get(id) {
|
|
1215
|
+
const result = await this.db.query(
|
|
1216
|
+
`SELECT * FROM embedding_set WHERE id = $1`,
|
|
1429
1217
|
[id]
|
|
1430
1218
|
);
|
|
1219
|
+
if (result.rows.length === 0) throw new Error(`Embedding set not found: ${id}`);
|
|
1220
|
+
return result.rows[0];
|
|
1221
|
+
}
|
|
1222
|
+
async list() {
|
|
1223
|
+
const result = await this.db.query(
|
|
1224
|
+
`SELECT * FROM embedding_set ORDER BY created_at, name`
|
|
1225
|
+
);
|
|
1226
|
+
return result.rows;
|
|
1227
|
+
}
|
|
1228
|
+
async listDescriptors() {
|
|
1229
|
+
const rows = await this.list();
|
|
1230
|
+
return rows.map((row) => this.toDescriptor(row));
|
|
1231
|
+
}
|
|
1232
|
+
toDescriptor(row) {
|
|
1431
1233
|
return {
|
|
1432
1234
|
id: row.id,
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
deleted_at: row.deleted_at,
|
|
1445
|
-
tags: tagsResult.rows.map((r) => r.tag),
|
|
1446
|
-
original: {
|
|
1447
|
-
id: row.original_id,
|
|
1448
|
-
content: row.original_content,
|
|
1449
|
-
content_hash: row.content_hash,
|
|
1450
|
-
created_at: row.original_created_at
|
|
1451
|
-
},
|
|
1452
|
-
current: {
|
|
1453
|
-
content: row.current_content,
|
|
1454
|
-
ai_metadata: row.ai_metadata,
|
|
1455
|
-
generation_count: row.generation_count,
|
|
1456
|
-
model: row.model,
|
|
1457
|
-
is_user_edited: row.is_user_edited,
|
|
1458
|
-
updated_at: row.current_updated_at
|
|
1459
|
-
}
|
|
1235
|
+
name: row.name,
|
|
1236
|
+
purpose: row.purpose,
|
|
1237
|
+
kind: row.kind,
|
|
1238
|
+
mode: row.mode ?? void 0,
|
|
1239
|
+
model: row.model_name,
|
|
1240
|
+
dimension: row.dimensions,
|
|
1241
|
+
truncateDimension: row.truncate_dimension,
|
|
1242
|
+
criteria: asObject(row.criteria_json),
|
|
1243
|
+
createdAt: dateString(row.created_at),
|
|
1244
|
+
updatedAt: dateString(row.updated_at),
|
|
1245
|
+
freshness: asObject(row.freshness_json) ?? { status: "fresh" }
|
|
1460
1246
|
};
|
|
1461
1247
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
async list(options = {}) {
|
|
1467
|
-
const {
|
|
1468
|
-
limit = 50,
|
|
1469
|
-
offset = 0,
|
|
1470
|
-
sort = "created_at",
|
|
1471
|
-
order = "desc",
|
|
1472
|
-
is_starred,
|
|
1473
|
-
is_pinned,
|
|
1474
|
-
is_archived,
|
|
1475
|
-
include_deleted = false,
|
|
1476
|
-
collection_id,
|
|
1477
|
-
tags
|
|
1478
|
-
} = options;
|
|
1479
|
-
const conditions = [];
|
|
1480
|
-
const params = [];
|
|
1481
|
-
let paramIndex = 1;
|
|
1482
|
-
if (!include_deleted) {
|
|
1483
|
-
conditions.push("n.deleted_at IS NULL");
|
|
1484
|
-
}
|
|
1485
|
-
if (is_starred !== void 0) {
|
|
1486
|
-
conditions.push(`n.is_starred = $${paramIndex++}`);
|
|
1487
|
-
params.push(is_starred);
|
|
1488
|
-
}
|
|
1489
|
-
if (is_pinned !== void 0) {
|
|
1490
|
-
conditions.push(`n.is_pinned = $${paramIndex++}`);
|
|
1491
|
-
params.push(is_pinned);
|
|
1492
|
-
}
|
|
1493
|
-
if (is_archived !== void 0) {
|
|
1494
|
-
conditions.push(`n.is_archived = $${paramIndex++}`);
|
|
1495
|
-
params.push(is_archived);
|
|
1496
|
-
}
|
|
1497
|
-
if (collection_id) {
|
|
1498
|
-
conditions.push(
|
|
1499
|
-
`EXISTS (SELECT 1 FROM collection_note cn WHERE cn.note_id = n.id AND cn.collection_id = $${paramIndex++})`
|
|
1500
|
-
);
|
|
1501
|
-
params.push(collection_id);
|
|
1248
|
+
async putEmbedding(input) {
|
|
1249
|
+
const set = await this.get(input.embedding_set_id);
|
|
1250
|
+
if (set.kind === "virtual") {
|
|
1251
|
+
throw new Error(`Cannot store vectors directly in virtual embedding set: ${set.id}`);
|
|
1502
1252
|
}
|
|
1503
|
-
if (
|
|
1504
|
-
|
|
1505
|
-
`
|
|
1253
|
+
if (input.vector.length !== set.dimensions) {
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
`Embedding vector has ${input.vector.length} dimensions; set ${set.id} expects ${set.dimensions}`
|
|
1506
1256
|
);
|
|
1507
|
-
params.push(tags);
|
|
1508
1257
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
const sortDir = order === "asc" ? "ASC" : "DESC";
|
|
1513
|
-
const countResult = await this.db.query(
|
|
1514
|
-
`SELECT COUNT(*) AS count FROM note n ${where}`,
|
|
1515
|
-
params
|
|
1258
|
+
await this.db.query(
|
|
1259
|
+
`DELETE FROM embedding_set_member WHERE note_id = $1 AND embedding_set_id = $2`,
|
|
1260
|
+
[input.note_id, input.embedding_set_id]
|
|
1516
1261
|
);
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
`SELECT n.id, n.title, n.format, n.source, n.visibility,
|
|
1521
|
-
n.is_starred, n.is_pinned, n.is_archived,
|
|
1522
|
-
n.created_at, n.updated_at, n.deleted_at
|
|
1523
|
-
FROM note n ${where}
|
|
1524
|
-
ORDER BY n.${sortCol} ${sortDir}
|
|
1525
|
-
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
|
1526
|
-
listParams
|
|
1262
|
+
await this.db.query(
|
|
1263
|
+
`DELETE FROM embedding WHERE note_id = $1 AND embedding_set_id = $2`,
|
|
1264
|
+
[input.note_id, input.embedding_set_id]
|
|
1527
1265
|
);
|
|
1528
|
-
const
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1266
|
+
const embeddingId = input.id ?? generateId();
|
|
1267
|
+
const vector2 = `[${input.vector.join(",")}]`;
|
|
1268
|
+
await this.db.query(
|
|
1269
|
+
`INSERT INTO embedding (id, note_id, embedding_set_id, vector)
|
|
1270
|
+
VALUES ($1, $2, $3, $4::vector)`,
|
|
1271
|
+
[embeddingId, input.note_id, input.embedding_set_id, vector2]
|
|
1272
|
+
);
|
|
1273
|
+
await this.db.query(
|
|
1274
|
+
`INSERT INTO embedding_set_member (embedding_set_id, note_id, embedding_id)
|
|
1275
|
+
VALUES ($1, $2, $3)`,
|
|
1276
|
+
[input.embedding_set_id, input.note_id, embeddingId]
|
|
1277
|
+
);
|
|
1278
|
+
return { id: embeddingId };
|
|
1279
|
+
}
|
|
1280
|
+
async resolveSelector(selector) {
|
|
1281
|
+
if (selector.kind === "default") {
|
|
1282
|
+
const set = await this.ensureDefault();
|
|
1283
|
+
return this.resolvePhysicalSet({ kind: "embedding-set", embeddingSetId: set.id }, set.id);
|
|
1284
|
+
}
|
|
1285
|
+
if (selector.kind === "embedding-set") {
|
|
1286
|
+
if (!selector.embeddingSetId) throw new Error("embedding-set selector requires embeddingSetId");
|
|
1287
|
+
const set = await this.get(selector.embeddingSetId);
|
|
1288
|
+
if (set.kind === "virtual") {
|
|
1289
|
+
const definition = this.definitionFromRow(set);
|
|
1290
|
+
return this.resolveDefinition({ kind: "embedding-set", embeddingSetId: set.id }, definition, set);
|
|
1539
1291
|
}
|
|
1292
|
+
return this.resolvePhysicalSet(selector, set.id);
|
|
1540
1293
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
title: r.title,
|
|
1544
|
-
format: r.format,
|
|
1545
|
-
source: r.source,
|
|
1546
|
-
visibility: r.visibility,
|
|
1547
|
-
is_starred: r.is_starred,
|
|
1548
|
-
is_pinned: r.is_pinned,
|
|
1549
|
-
is_archived: r.is_archived,
|
|
1550
|
-
created_at: r.created_at,
|
|
1551
|
-
updated_at: r.updated_at,
|
|
1552
|
-
deleted_at: r.deleted_at,
|
|
1553
|
-
tags: tagMap.get(r.id) ?? []
|
|
1554
|
-
}));
|
|
1555
|
-
return { items, total, limit, offset };
|
|
1294
|
+
if (!selector.definition) throw new Error("virtual-definition selector requires definition");
|
|
1295
|
+
return this.resolveDefinition(selector, selector.definition);
|
|
1556
1296
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
setClauses.push(`format = $${paramIdx++}`);
|
|
1578
|
-
noteParams.push(input.format);
|
|
1579
|
-
}
|
|
1580
|
-
if (input.visibility !== void 0) {
|
|
1581
|
-
setClauses.push(`visibility = $${paramIdx++}`);
|
|
1582
|
-
noteParams.push(input.visibility);
|
|
1583
|
-
}
|
|
1584
|
-
noteParams.push(id);
|
|
1585
|
-
await tx.query(
|
|
1586
|
-
`UPDATE note SET ${setClauses.join(", ")} WHERE id = $${paramIdx}`,
|
|
1587
|
-
noteParams
|
|
1297
|
+
async refreshMaterializedVirtualSet(setId) {
|
|
1298
|
+
const set = await this.get(setId);
|
|
1299
|
+
if (set.kind !== "virtual") throw new Error(`Embedding set is not virtual: ${setId}`);
|
|
1300
|
+
const definition = this.definitionFromRow(set);
|
|
1301
|
+
if (!definition.materialization?.allowed) {
|
|
1302
|
+
throw new Error(`Virtual embedding set does not allow materialization: ${setId}`);
|
|
1303
|
+
}
|
|
1304
|
+
const live = await this.resolveDefinition(
|
|
1305
|
+
{ kind: "embedding-set", embeddingSetId: setId },
|
|
1306
|
+
definition,
|
|
1307
|
+
set,
|
|
1308
|
+
{ forceLive: true }
|
|
1309
|
+
);
|
|
1310
|
+
await this.db.query(`DELETE FROM embedding_set_member WHERE embedding_set_id = $1`, [setId]);
|
|
1311
|
+
for (const row of live.rows) {
|
|
1312
|
+
await this.db.query(
|
|
1313
|
+
`INSERT INTO embedding_set_member (embedding_set_id, note_id, embedding_id)
|
|
1314
|
+
VALUES ($1, $2, $3)
|
|
1315
|
+
ON CONFLICT DO NOTHING`,
|
|
1316
|
+
[setId, row.note_id, row.embedding_id]
|
|
1588
1317
|
);
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
);
|
|
1607
|
-
}
|
|
1608
|
-
});
|
|
1609
|
-
this.events?.emit("note.updated", { id });
|
|
1610
|
-
return this.get(id);
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* Soft-delete a note by setting deleted_at to the current timestamp.
|
|
1614
|
-
*/
|
|
1615
|
-
async delete(id) {
|
|
1318
|
+
}
|
|
1319
|
+
const inputHash = this.resolutionInputHash(definition, live.rows);
|
|
1320
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1321
|
+
const materialization = {
|
|
1322
|
+
...definition.materialization,
|
|
1323
|
+
allowed: true,
|
|
1324
|
+
includeResolvedMembers: true,
|
|
1325
|
+
freshness: "fresh",
|
|
1326
|
+
inputHash,
|
|
1327
|
+
generatedAt,
|
|
1328
|
+
resolvedMemberCount: live.rows.length
|
|
1329
|
+
};
|
|
1330
|
+
const freshness = {
|
|
1331
|
+
status: "fresh",
|
|
1332
|
+
sourceHash: inputHash,
|
|
1333
|
+
checkedAt: generatedAt
|
|
1334
|
+
};
|
|
1616
1335
|
await this.db.query(
|
|
1617
|
-
`UPDATE
|
|
1618
|
-
|
|
1336
|
+
`UPDATE embedding_set
|
|
1337
|
+
SET materialization_json = $2::jsonb, freshness_json = $3::jsonb, updated_at = now()
|
|
1338
|
+
WHERE id = $1`,
|
|
1339
|
+
[setId, jsonParam(materialization), jsonParam(freshness)]
|
|
1619
1340
|
);
|
|
1620
|
-
this.
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
`UPDATE note SET deleted_at = NULL, updated_at = now() WHERE id = $1`,
|
|
1628
|
-
[id]
|
|
1341
|
+
return this.finalizeResolution(
|
|
1342
|
+
{ kind: "embedding-set", embeddingSetId: setId },
|
|
1343
|
+
live.rows,
|
|
1344
|
+
live.errors,
|
|
1345
|
+
definition.compatibility,
|
|
1346
|
+
"fresh",
|
|
1347
|
+
"materialized"
|
|
1629
1348
|
);
|
|
1630
|
-
this.events?.emit("note.restored", { id });
|
|
1631
|
-
return this.get(id);
|
|
1632
1349
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1350
|
+
async markVirtualSetStale(setId, reason) {
|
|
1351
|
+
const set = await this.get(setId);
|
|
1352
|
+
if (set.kind !== "virtual") throw new Error(`Embedding set is not virtual: ${setId}`);
|
|
1353
|
+
const definition = this.definitionFromRow(set);
|
|
1354
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1355
|
+
const materialization = definition.materialization ? { ...definition.materialization, freshness: "stale" } : null;
|
|
1637
1356
|
await this.db.query(
|
|
1638
|
-
`UPDATE
|
|
1639
|
-
|
|
1357
|
+
`UPDATE embedding_set
|
|
1358
|
+
SET materialization_json = $2::jsonb, freshness_json = $3::jsonb, updated_at = now()
|
|
1359
|
+
WHERE id = $1`,
|
|
1360
|
+
[
|
|
1361
|
+
setId,
|
|
1362
|
+
jsonParam(materialization),
|
|
1363
|
+
jsonParam({ status: "stale", sourceHash: definition.materialization?.inputHash, checkedAt: now, reason })
|
|
1364
|
+
]
|
|
1640
1365
|
);
|
|
1641
|
-
this.events?.emit("note.updated", { id });
|
|
1642
1366
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1367
|
+
async resolveDefinition(selector, definition, set, options = {}) {
|
|
1368
|
+
if (!options.forceLive && set && definition.materialization?.allowed && definition.materialization.freshness === "fresh") {
|
|
1369
|
+
const materialized = await this.resolveMaterializedRows(set.id);
|
|
1370
|
+
if (materialized.length > 0 || definition.materialization.resolvedMemberCount === 0) {
|
|
1371
|
+
return this.finalizeResolution(
|
|
1372
|
+
selector,
|
|
1373
|
+
materialized,
|
|
1374
|
+
[],
|
|
1375
|
+
definition.compatibility,
|
|
1376
|
+
"fresh",
|
|
1377
|
+
"materialized"
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
let rows;
|
|
1382
|
+
const errors = [];
|
|
1383
|
+
switch (definition.source.type) {
|
|
1384
|
+
case "criteria":
|
|
1385
|
+
rows = await this.resolveCriteriaSource(definition.source);
|
|
1386
|
+
break;
|
|
1387
|
+
case "set-operation":
|
|
1388
|
+
rows = await this.resolveSetOperationSource(definition.source, definition.compatibility, errors);
|
|
1389
|
+
break;
|
|
1390
|
+
case "fallback":
|
|
1391
|
+
rows = await this.resolveFallbackSource(definition.source.preferredSetIds, definition.compatibility, errors);
|
|
1392
|
+
break;
|
|
1393
|
+
case "latest-compatible":
|
|
1394
|
+
rows = await this.resolveLatestCompatibleSource(definition.source, definition.compatibility, errors);
|
|
1395
|
+
break;
|
|
1396
|
+
case "snapshot":
|
|
1397
|
+
rows = await this.resolvePhysicalRows(definition.source.snapshotId);
|
|
1398
|
+
break;
|
|
1399
|
+
default:
|
|
1400
|
+
rows = [];
|
|
1401
|
+
}
|
|
1402
|
+
return this.finalizeResolution(selector, rows, errors, definition.compatibility, definition.materialization?.freshness ?? "unknown", "live");
|
|
1652
1403
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
*/
|
|
1656
|
-
async archive(id, archived) {
|
|
1657
|
-
await this.db.query(
|
|
1658
|
-
`UPDATE note SET is_archived = $1, updated_at = now() WHERE id = $2`,
|
|
1659
|
-
[archived, id]
|
|
1660
|
-
);
|
|
1661
|
-
this.events?.emit("note.updated", { id });
|
|
1404
|
+
async resolvePhysicalSet(selector, setId) {
|
|
1405
|
+
return this.finalizeResolution(selector, await this.resolvePhysicalRows(setId), [], DEFAULT_COMPATIBILITY, "fresh", "live");
|
|
1662
1406
|
}
|
|
1663
|
-
|
|
1664
|
-
* Get revision history for a note, ordered by revision_number descending.
|
|
1665
|
-
*/
|
|
1666
|
-
async getRevisions(noteId) {
|
|
1407
|
+
async resolvePhysicalRows(setId) {
|
|
1667
1408
|
const result = await this.db.query(
|
|
1668
|
-
`SELECT
|
|
1669
|
-
FROM
|
|
1670
|
-
WHERE
|
|
1671
|
-
ORDER BY
|
|
1672
|
-
[
|
|
1409
|
+
`SELECT note_id, embedding_set_id, id as embedding_id, vector::text as vector, created_at
|
|
1410
|
+
FROM embedding
|
|
1411
|
+
WHERE embedding_set_id = $1
|
|
1412
|
+
ORDER BY note_id, created_at DESC`,
|
|
1413
|
+
[setId]
|
|
1673
1414
|
);
|
|
1674
1415
|
return result.rows;
|
|
1675
1416
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
conditions.push("n.deleted_at IS NULL");
|
|
1685
|
-
}
|
|
1686
|
-
if (options.tags?.length) {
|
|
1687
|
-
conditions.push(
|
|
1688
|
-
`EXISTS (SELECT 1 FROM note_tag nt WHERE nt.note_id = n.id AND nt.tag = ANY($${idx++}))`
|
|
1689
|
-
);
|
|
1690
|
-
params.push(options.tags);
|
|
1691
|
-
}
|
|
1692
|
-
if (options.collection_id) {
|
|
1693
|
-
conditions.push(
|
|
1694
|
-
`EXISTS (SELECT 1 FROM collection_note cn WHERE cn.note_id = n.id AND cn.collection_id = $${idx++})`
|
|
1417
|
+
async resolveMaterializedRows(setId) {
|
|
1418
|
+
const result = await this.db.query(
|
|
1419
|
+
`SELECT e.note_id, e.embedding_set_id, e.id as embedding_id, e.vector::text as vector, e.created_at
|
|
1420
|
+
FROM embedding_set_member m
|
|
1421
|
+
JOIN embedding e ON e.id = m.embedding_id
|
|
1422
|
+
WHERE m.embedding_set_id = $1
|
|
1423
|
+
ORDER BY e.note_id, e.created_at DESC`,
|
|
1424
|
+
[setId]
|
|
1695
1425
|
);
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
if (options.date_from) {
|
|
1699
|
-
conditions.push(`n.created_at >= $${idx++}`);
|
|
1700
|
-
params.push(options.date_from);
|
|
1701
|
-
}
|
|
1702
|
-
if (options.date_to) {
|
|
1703
|
-
conditions.push(`n.created_at <= $${idx++}`);
|
|
1704
|
-
params.push(options.date_to);
|
|
1705
|
-
}
|
|
1706
|
-
if (options.is_starred !== void 0) {
|
|
1707
|
-
conditions.push(`n.is_starred = $${idx++}`);
|
|
1708
|
-
params.push(options.is_starred);
|
|
1709
|
-
}
|
|
1710
|
-
if (options.is_archived !== void 0) {
|
|
1711
|
-
conditions.push(`n.is_archived = $${idx++}`);
|
|
1712
|
-
params.push(options.is_archived);
|
|
1713
|
-
}
|
|
1714
|
-
if (options.format) {
|
|
1715
|
-
conditions.push(`n.format = $${idx++}`);
|
|
1716
|
-
params.push(options.format);
|
|
1717
|
-
}
|
|
1718
|
-
if (options.source) {
|
|
1719
|
-
conditions.push(`n.source = $${idx++}`);
|
|
1720
|
-
params.push(options.source);
|
|
1721
|
-
}
|
|
1722
|
-
if (options.visibility) {
|
|
1723
|
-
conditions.push(`n.visibility = $${idx++}`);
|
|
1724
|
-
params.push(options.visibility);
|
|
1426
|
+
return result.rows;
|
|
1725
1427
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
};
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
)
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
const row = await this.get(id);
|
|
1802
|
-
if (input.materialization?.allowed) {
|
|
1803
|
-
await this.refreshMaterializedVirtualSet(id);
|
|
1804
|
-
return this.get(id);
|
|
1428
|
+
async resolveCriteriaSource(source) {
|
|
1429
|
+
const criteria = source.criteria;
|
|
1430
|
+
if (criteria.conceptIds && criteria.conceptIds.length > 0) {
|
|
1431
|
+
throw new Error("Unsupported virtual embedding-set criteria field: conceptIds");
|
|
1432
|
+
}
|
|
1433
|
+
const conditions = ["e.embedding_set_id = $1"];
|
|
1434
|
+
const params = [source.baseSetId];
|
|
1435
|
+
let idx = 2;
|
|
1436
|
+
if (criteria.noteIds?.length) {
|
|
1437
|
+
conditions.push(`n.id = ANY($${idx++})`);
|
|
1438
|
+
params.push(criteria.noteIds);
|
|
1439
|
+
}
|
|
1440
|
+
if (criteria.tags?.length) {
|
|
1441
|
+
conditions.push(`EXISTS (SELECT 1 FROM note_tag nt WHERE nt.note_id = n.id AND nt.tag = ANY($${idx++}))`);
|
|
1442
|
+
params.push(criteria.tags);
|
|
1443
|
+
}
|
|
1444
|
+
if (criteria.collectionIds?.length) {
|
|
1445
|
+
conditions.push(`EXISTS (SELECT 1 FROM collection_note cn WHERE cn.note_id = n.id AND cn.collection_id = ANY($${idx++}))`);
|
|
1446
|
+
params.push(criteria.collectionIds);
|
|
1447
|
+
}
|
|
1448
|
+
if (criteria.sources?.length) {
|
|
1449
|
+
conditions.push(`n.source = ANY($${idx++})`);
|
|
1450
|
+
params.push(criteria.sources);
|
|
1451
|
+
}
|
|
1452
|
+
if (criteria.formats?.length) {
|
|
1453
|
+
conditions.push(`n.format = ANY($${idx++})`);
|
|
1454
|
+
params.push(criteria.formats);
|
|
1455
|
+
}
|
|
1456
|
+
if (criteria.visibilities?.length) {
|
|
1457
|
+
conditions.push(`n.visibility = ANY($${idx++})`);
|
|
1458
|
+
params.push(criteria.visibilities);
|
|
1459
|
+
}
|
|
1460
|
+
if (criteria.isStarred !== void 0) {
|
|
1461
|
+
conditions.push(`n.is_starred = $${idx++}`);
|
|
1462
|
+
params.push(criteria.isStarred);
|
|
1463
|
+
}
|
|
1464
|
+
if (criteria.isArchived !== void 0) {
|
|
1465
|
+
conditions.push(`n.is_archived = $${idx++}`);
|
|
1466
|
+
params.push(criteria.isArchived);
|
|
1467
|
+
}
|
|
1468
|
+
if (criteria.hasTitle !== void 0) {
|
|
1469
|
+
conditions.push(criteria.hasTitle ? `n.title IS NOT NULL AND n.title <> ''` : `(n.title IS NULL OR n.title = '')`);
|
|
1470
|
+
}
|
|
1471
|
+
if (criteria.hasEmbedding === false) {
|
|
1472
|
+
conditions.push("FALSE");
|
|
1473
|
+
}
|
|
1474
|
+
if (criteria.isUserEdited !== void 0) {
|
|
1475
|
+
conditions.push(`COALESCE(c.is_user_edited, false) = $${idx++}`);
|
|
1476
|
+
params.push(criteria.isUserEdited);
|
|
1477
|
+
}
|
|
1478
|
+
if (criteria.hasAiMetadata !== void 0) {
|
|
1479
|
+
conditions.push(criteria.hasAiMetadata ? `c.ai_metadata IS NOT NULL` : `c.ai_metadata IS NULL`);
|
|
1480
|
+
}
|
|
1481
|
+
if (criteria.hasRevisions !== void 0) {
|
|
1482
|
+
conditions.push(criteria.hasRevisions ? `EXISTS (SELECT 1 FROM note_revision nr WHERE nr.note_id = n.id)` : `NOT EXISTS (SELECT 1 FROM note_revision nr WHERE nr.note_id = n.id)`);
|
|
1483
|
+
}
|
|
1484
|
+
if (criteria.minGenerationCount !== void 0) {
|
|
1485
|
+
conditions.push(`COALESCE(c.generation_count, 0) >= $${idx++}`);
|
|
1486
|
+
params.push(criteria.minGenerationCount);
|
|
1487
|
+
}
|
|
1488
|
+
if (criteria.maxGenerationCount !== void 0) {
|
|
1489
|
+
conditions.push(`COALESCE(c.generation_count, 0) <= $${idx++}`);
|
|
1490
|
+
params.push(criteria.maxGenerationCount);
|
|
1491
|
+
}
|
|
1492
|
+
if (criteria.updatedAfter) {
|
|
1493
|
+
conditions.push(`n.updated_at >= $${idx++}`);
|
|
1494
|
+
params.push(criteria.updatedAfter);
|
|
1495
|
+
}
|
|
1496
|
+
if (criteria.updatedBefore) {
|
|
1497
|
+
conditions.push(`n.updated_at <= $${idx++}`);
|
|
1498
|
+
params.push(criteria.updatedBefore);
|
|
1499
|
+
}
|
|
1500
|
+
if (criteria.query?.trim()) {
|
|
1501
|
+
conditions.push(`(n.tsv @@ plainto_tsquery('english', $${idx}) OR to_tsvector('english', coalesce(c.content, '')) @@ plainto_tsquery('english', $${idx}))`);
|
|
1502
|
+
params.push(criteria.query);
|
|
1805
1503
|
}
|
|
1806
|
-
return row;
|
|
1807
|
-
}
|
|
1808
|
-
async ensureDefault() {
|
|
1809
|
-
const existing = await this.db.query(
|
|
1810
|
-
`SELECT * FROM embedding_set WHERE name = $1 AND model_name = $2 AND kind = 'physical' ORDER BY created_at LIMIT 1`,
|
|
1811
|
-
["Full content", "all-MiniLM-L6-v2"]
|
|
1812
|
-
);
|
|
1813
|
-
if (existing.rows.length > 0) return existing.rows[0];
|
|
1814
|
-
return this.create({
|
|
1815
|
-
name: "Full content",
|
|
1816
|
-
purpose: "Semantic search over full revised note content",
|
|
1817
|
-
model_name: "all-MiniLM-L6-v2",
|
|
1818
|
-
dimensions: 384,
|
|
1819
|
-
kind: "physical"
|
|
1820
|
-
});
|
|
1821
|
-
}
|
|
1822
|
-
async get(id) {
|
|
1823
|
-
const result = await this.db.query(
|
|
1824
|
-
`SELECT * FROM embedding_set WHERE id = $1`,
|
|
1825
|
-
[id]
|
|
1826
|
-
);
|
|
1827
|
-
if (result.rows.length === 0) throw new Error(`Embedding set not found: ${id}`);
|
|
1828
|
-
return result.rows[0];
|
|
1829
|
-
}
|
|
1830
|
-
async list() {
|
|
1831
1504
|
const result = await this.db.query(
|
|
1832
|
-
`SELECT
|
|
1505
|
+
`SELECT e.note_id, e.embedding_set_id, e.id as embedding_id, e.vector::text as vector, e.created_at
|
|
1506
|
+
FROM embedding e
|
|
1507
|
+
JOIN note n ON n.id = e.note_id
|
|
1508
|
+
LEFT JOIN note_revised_current c ON c.note_id = n.id
|
|
1509
|
+
WHERE ${conditions.join(" AND ")}
|
|
1510
|
+
ORDER BY e.note_id, e.created_at DESC`,
|
|
1511
|
+
params
|
|
1833
1512
|
);
|
|
1834
1513
|
return result.rows;
|
|
1835
1514
|
}
|
|
1836
|
-
async
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
kind: row.kind,
|
|
1846
|
-
mode: row.mode ?? void 0,
|
|
1847
|
-
model: row.model_name,
|
|
1848
|
-
dimension: row.dimensions,
|
|
1849
|
-
truncateDimension: row.truncate_dimension,
|
|
1850
|
-
criteria: asObject(row.criteria_json),
|
|
1851
|
-
createdAt: dateString(row.created_at),
|
|
1852
|
-
updatedAt: dateString(row.updated_at),
|
|
1853
|
-
freshness: asObject(row.freshness_json) ?? { status: "fresh" }
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
async putEmbedding(input) {
|
|
1857
|
-
const set = await this.get(input.embedding_set_id);
|
|
1858
|
-
if (set.kind === "virtual") {
|
|
1859
|
-
throw new Error(`Cannot store vectors directly in virtual embedding set: ${set.id}`);
|
|
1515
|
+
async resolveSetOperationSource(source, compatibility, errors) {
|
|
1516
|
+
const bySet = /* @__PURE__ */ new Map();
|
|
1517
|
+
for (const setId of source.setIds) bySet.set(setId, await this.resolvePhysicalRows(setId));
|
|
1518
|
+
await this.validateCompatibility(source.setIds, compatibility, errors);
|
|
1519
|
+
const noteSets = source.setIds.map((setId) => new Set((bySet.get(setId) ?? []).map((row) => row.note_id)));
|
|
1520
|
+
const firstRows = bySet.get(source.setIds[0]) ?? [];
|
|
1521
|
+
if (source.operation === "difference") {
|
|
1522
|
+
const excluded = new Set(noteSets.slice(1).flatMap((set) => Array.from(set)));
|
|
1523
|
+
return firstRows.filter((row) => !excluded.has(row.note_id));
|
|
1860
1524
|
}
|
|
1861
|
-
if (
|
|
1862
|
-
|
|
1863
|
-
`Embedding vector has ${input.vector.length} dimensions; set ${set.id} expects ${set.dimensions}`
|
|
1864
|
-
);
|
|
1525
|
+
if (source.operation === "intersection") {
|
|
1526
|
+
return firstRows.filter((row) => noteSets.every((set) => set.has(row.note_id)));
|
|
1865
1527
|
}
|
|
1866
|
-
|
|
1867
|
-
`DELETE FROM embedding_set_member WHERE note_id = $1 AND embedding_set_id = $2`,
|
|
1868
|
-
[input.note_id, input.embedding_set_id]
|
|
1869
|
-
);
|
|
1870
|
-
await this.db.query(
|
|
1871
|
-
`DELETE FROM embedding WHERE note_id = $1 AND embedding_set_id = $2`,
|
|
1872
|
-
[input.note_id, input.embedding_set_id]
|
|
1873
|
-
);
|
|
1874
|
-
const embeddingId = input.id ?? generateId();
|
|
1875
|
-
const vector2 = `[${input.vector.join(",")}]`;
|
|
1876
|
-
await this.db.query(
|
|
1877
|
-
`INSERT INTO embedding (id, note_id, embedding_set_id, vector)
|
|
1878
|
-
VALUES ($1, $2, $3, $4::vector)`,
|
|
1879
|
-
[embeddingId, input.note_id, input.embedding_set_id, vector2]
|
|
1880
|
-
);
|
|
1881
|
-
await this.db.query(
|
|
1882
|
-
`INSERT INTO embedding_set_member (embedding_set_id, note_id, embedding_id)
|
|
1883
|
-
VALUES ($1, $2, $3)`,
|
|
1884
|
-
[input.embedding_set_id, input.note_id, embeddingId]
|
|
1885
|
-
);
|
|
1886
|
-
return { id: embeddingId };
|
|
1528
|
+
return this.resolveDuplicateRows(source.setIds.flatMap((setId) => bySet.get(setId) ?? []), compatibility, errors);
|
|
1887
1529
|
}
|
|
1888
|
-
async
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
const set = await this.get(selector.embeddingSetId);
|
|
1896
|
-
if (set.kind === "virtual") {
|
|
1897
|
-
const definition = this.definitionFromRow(set);
|
|
1898
|
-
return this.resolveDefinition({ kind: "embedding-set", embeddingSetId: set.id }, definition, set);
|
|
1899
|
-
}
|
|
1900
|
-
return this.resolvePhysicalSet(selector, set.id);
|
|
1901
|
-
}
|
|
1902
|
-
if (!selector.definition) throw new Error("virtual-definition selector requires definition");
|
|
1903
|
-
return this.resolveDefinition(selector, selector.definition);
|
|
1904
|
-
}
|
|
1905
|
-
async refreshMaterializedVirtualSet(setId) {
|
|
1906
|
-
const set = await this.get(setId);
|
|
1907
|
-
if (set.kind !== "virtual") throw new Error(`Embedding set is not virtual: ${setId}`);
|
|
1908
|
-
const definition = this.definitionFromRow(set);
|
|
1909
|
-
if (!definition.materialization?.allowed) {
|
|
1910
|
-
throw new Error(`Virtual embedding set does not allow materialization: ${setId}`);
|
|
1911
|
-
}
|
|
1912
|
-
const live = await this.resolveDefinition(
|
|
1913
|
-
{ kind: "embedding-set", embeddingSetId: setId },
|
|
1914
|
-
definition,
|
|
1915
|
-
set,
|
|
1916
|
-
{ forceLive: true }
|
|
1917
|
-
);
|
|
1918
|
-
await this.db.query(`DELETE FROM embedding_set_member WHERE embedding_set_id = $1`, [setId]);
|
|
1919
|
-
for (const row of live.rows) {
|
|
1920
|
-
await this.db.query(
|
|
1921
|
-
`INSERT INTO embedding_set_member (embedding_set_id, note_id, embedding_id)
|
|
1922
|
-
VALUES ($1, $2, $3)
|
|
1923
|
-
ON CONFLICT DO NOTHING`,
|
|
1924
|
-
[setId, row.note_id, row.embedding_id]
|
|
1925
|
-
);
|
|
1926
|
-
}
|
|
1927
|
-
const inputHash = this.resolutionInputHash(definition, live.rows);
|
|
1928
|
-
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1929
|
-
const materialization = {
|
|
1930
|
-
...definition.materialization,
|
|
1931
|
-
allowed: true,
|
|
1932
|
-
includeResolvedMembers: true,
|
|
1933
|
-
freshness: "fresh",
|
|
1934
|
-
inputHash,
|
|
1935
|
-
generatedAt,
|
|
1936
|
-
resolvedMemberCount: live.rows.length
|
|
1937
|
-
};
|
|
1938
|
-
const freshness = {
|
|
1939
|
-
status: "fresh",
|
|
1940
|
-
sourceHash: inputHash,
|
|
1941
|
-
checkedAt: generatedAt
|
|
1942
|
-
};
|
|
1943
|
-
await this.db.query(
|
|
1944
|
-
`UPDATE embedding_set
|
|
1945
|
-
SET materialization_json = $2::jsonb, freshness_json = $3::jsonb, updated_at = now()
|
|
1946
|
-
WHERE id = $1`,
|
|
1947
|
-
[setId, jsonParam(materialization), jsonParam(freshness)]
|
|
1948
|
-
);
|
|
1949
|
-
return this.finalizeResolution(
|
|
1950
|
-
{ kind: "embedding-set", embeddingSetId: setId },
|
|
1951
|
-
live.rows,
|
|
1952
|
-
live.errors,
|
|
1953
|
-
definition.compatibility,
|
|
1954
|
-
"fresh",
|
|
1955
|
-
"materialized"
|
|
1956
|
-
);
|
|
1957
|
-
}
|
|
1958
|
-
async markVirtualSetStale(setId, reason) {
|
|
1959
|
-
const set = await this.get(setId);
|
|
1960
|
-
if (set.kind !== "virtual") throw new Error(`Embedding set is not virtual: ${setId}`);
|
|
1961
|
-
const definition = this.definitionFromRow(set);
|
|
1962
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1963
|
-
const materialization = definition.materialization ? { ...definition.materialization, freshness: "stale" } : null;
|
|
1964
|
-
await this.db.query(
|
|
1965
|
-
`UPDATE embedding_set
|
|
1966
|
-
SET materialization_json = $2::jsonb, freshness_json = $3::jsonb, updated_at = now()
|
|
1967
|
-
WHERE id = $1`,
|
|
1968
|
-
[
|
|
1969
|
-
setId,
|
|
1970
|
-
jsonParam(materialization),
|
|
1971
|
-
jsonParam({ status: "stale", sourceHash: definition.materialization?.inputHash, checkedAt: now, reason })
|
|
1972
|
-
]
|
|
1973
|
-
);
|
|
1974
|
-
}
|
|
1975
|
-
async resolveDefinition(selector, definition, set, options = {}) {
|
|
1976
|
-
if (!options.forceLive && set && definition.materialization?.allowed && definition.materialization.freshness === "fresh") {
|
|
1977
|
-
const materialized = await this.resolveMaterializedRows(set.id);
|
|
1978
|
-
if (materialized.length > 0 || definition.materialization.resolvedMemberCount === 0) {
|
|
1979
|
-
return this.finalizeResolution(
|
|
1980
|
-
selector,
|
|
1981
|
-
materialized,
|
|
1982
|
-
[],
|
|
1983
|
-
definition.compatibility,
|
|
1984
|
-
"fresh",
|
|
1985
|
-
"materialized"
|
|
1986
|
-
);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
let rows;
|
|
1990
|
-
const errors = [];
|
|
1991
|
-
switch (definition.source.type) {
|
|
1992
|
-
case "criteria":
|
|
1993
|
-
rows = await this.resolveCriteriaSource(definition.source);
|
|
1994
|
-
break;
|
|
1995
|
-
case "set-operation":
|
|
1996
|
-
rows = await this.resolveSetOperationSource(definition.source, definition.compatibility, errors);
|
|
1997
|
-
break;
|
|
1998
|
-
case "fallback":
|
|
1999
|
-
rows = await this.resolveFallbackSource(definition.source.preferredSetIds, definition.compatibility, errors);
|
|
2000
|
-
break;
|
|
2001
|
-
case "latest-compatible":
|
|
2002
|
-
rows = await this.resolveLatestCompatibleSource(definition.source, definition.compatibility, errors);
|
|
2003
|
-
break;
|
|
2004
|
-
case "snapshot":
|
|
2005
|
-
rows = await this.resolvePhysicalRows(definition.source.snapshotId);
|
|
2006
|
-
break;
|
|
2007
|
-
default:
|
|
2008
|
-
rows = [];
|
|
2009
|
-
}
|
|
2010
|
-
return this.finalizeResolution(selector, rows, errors, definition.compatibility, definition.materialization?.freshness ?? "unknown", "live");
|
|
2011
|
-
}
|
|
2012
|
-
async resolvePhysicalSet(selector, setId) {
|
|
2013
|
-
return this.finalizeResolution(selector, await this.resolvePhysicalRows(setId), [], DEFAULT_COMPATIBILITY, "fresh", "live");
|
|
2014
|
-
}
|
|
2015
|
-
async resolvePhysicalRows(setId) {
|
|
2016
|
-
const result = await this.db.query(
|
|
2017
|
-
`SELECT note_id, embedding_set_id, id as embedding_id, vector::text as vector, created_at
|
|
2018
|
-
FROM embedding
|
|
2019
|
-
WHERE embedding_set_id = $1
|
|
2020
|
-
ORDER BY note_id, created_at DESC`,
|
|
2021
|
-
[setId]
|
|
2022
|
-
);
|
|
2023
|
-
return result.rows;
|
|
2024
|
-
}
|
|
2025
|
-
async resolveMaterializedRows(setId) {
|
|
2026
|
-
const result = await this.db.query(
|
|
2027
|
-
`SELECT e.note_id, e.embedding_set_id, e.id as embedding_id, e.vector::text as vector, e.created_at
|
|
2028
|
-
FROM embedding_set_member m
|
|
2029
|
-
JOIN embedding e ON e.id = m.embedding_id
|
|
2030
|
-
WHERE m.embedding_set_id = $1
|
|
2031
|
-
ORDER BY e.note_id, e.created_at DESC`,
|
|
2032
|
-
[setId]
|
|
2033
|
-
);
|
|
2034
|
-
return result.rows;
|
|
2035
|
-
}
|
|
2036
|
-
async resolveCriteriaSource(source) {
|
|
2037
|
-
const criteria = source.criteria;
|
|
2038
|
-
if (criteria.conceptIds && criteria.conceptIds.length > 0) {
|
|
2039
|
-
throw new Error("Unsupported virtual embedding-set criteria field: conceptIds");
|
|
2040
|
-
}
|
|
2041
|
-
const conditions = ["e.embedding_set_id = $1"];
|
|
2042
|
-
const params = [source.baseSetId];
|
|
2043
|
-
let idx = 2;
|
|
2044
|
-
if (criteria.noteIds?.length) {
|
|
2045
|
-
conditions.push(`n.id = ANY($${idx++})`);
|
|
2046
|
-
params.push(criteria.noteIds);
|
|
2047
|
-
}
|
|
2048
|
-
if (criteria.tags?.length) {
|
|
2049
|
-
conditions.push(`EXISTS (SELECT 1 FROM note_tag nt WHERE nt.note_id = n.id AND nt.tag = ANY($${idx++}))`);
|
|
2050
|
-
params.push(criteria.tags);
|
|
2051
|
-
}
|
|
2052
|
-
if (criteria.collectionIds?.length) {
|
|
2053
|
-
conditions.push(`EXISTS (SELECT 1 FROM collection_note cn WHERE cn.note_id = n.id AND cn.collection_id = ANY($${idx++}))`);
|
|
2054
|
-
params.push(criteria.collectionIds);
|
|
2055
|
-
}
|
|
2056
|
-
if (criteria.sources?.length) {
|
|
2057
|
-
conditions.push(`n.source = ANY($${idx++})`);
|
|
2058
|
-
params.push(criteria.sources);
|
|
2059
|
-
}
|
|
2060
|
-
if (criteria.formats?.length) {
|
|
2061
|
-
conditions.push(`n.format = ANY($${idx++})`);
|
|
2062
|
-
params.push(criteria.formats);
|
|
2063
|
-
}
|
|
2064
|
-
if (criteria.visibilities?.length) {
|
|
2065
|
-
conditions.push(`n.visibility = ANY($${idx++})`);
|
|
2066
|
-
params.push(criteria.visibilities);
|
|
2067
|
-
}
|
|
2068
|
-
if (criteria.isStarred !== void 0) {
|
|
2069
|
-
conditions.push(`n.is_starred = $${idx++}`);
|
|
2070
|
-
params.push(criteria.isStarred);
|
|
2071
|
-
}
|
|
2072
|
-
if (criteria.isArchived !== void 0) {
|
|
2073
|
-
conditions.push(`n.is_archived = $${idx++}`);
|
|
2074
|
-
params.push(criteria.isArchived);
|
|
2075
|
-
}
|
|
2076
|
-
if (criteria.hasTitle !== void 0) {
|
|
2077
|
-
conditions.push(criteria.hasTitle ? `n.title IS NOT NULL AND n.title <> ''` : `(n.title IS NULL OR n.title = '')`);
|
|
2078
|
-
}
|
|
2079
|
-
if (criteria.hasEmbedding === false) {
|
|
2080
|
-
conditions.push("FALSE");
|
|
2081
|
-
}
|
|
2082
|
-
if (criteria.isUserEdited !== void 0) {
|
|
2083
|
-
conditions.push(`COALESCE(c.is_user_edited, false) = $${idx++}`);
|
|
2084
|
-
params.push(criteria.isUserEdited);
|
|
2085
|
-
}
|
|
2086
|
-
if (criteria.hasAiMetadata !== void 0) {
|
|
2087
|
-
conditions.push(criteria.hasAiMetadata ? `c.ai_metadata IS NOT NULL` : `c.ai_metadata IS NULL`);
|
|
2088
|
-
}
|
|
2089
|
-
if (criteria.hasRevisions !== void 0) {
|
|
2090
|
-
conditions.push(criteria.hasRevisions ? `EXISTS (SELECT 1 FROM note_revision nr WHERE nr.note_id = n.id)` : `NOT EXISTS (SELECT 1 FROM note_revision nr WHERE nr.note_id = n.id)`);
|
|
2091
|
-
}
|
|
2092
|
-
if (criteria.minGenerationCount !== void 0) {
|
|
2093
|
-
conditions.push(`COALESCE(c.generation_count, 0) >= $${idx++}`);
|
|
2094
|
-
params.push(criteria.minGenerationCount);
|
|
2095
|
-
}
|
|
2096
|
-
if (criteria.maxGenerationCount !== void 0) {
|
|
2097
|
-
conditions.push(`COALESCE(c.generation_count, 0) <= $${idx++}`);
|
|
2098
|
-
params.push(criteria.maxGenerationCount);
|
|
2099
|
-
}
|
|
2100
|
-
if (criteria.updatedAfter) {
|
|
2101
|
-
conditions.push(`n.updated_at >= $${idx++}`);
|
|
2102
|
-
params.push(criteria.updatedAfter);
|
|
2103
|
-
}
|
|
2104
|
-
if (criteria.updatedBefore) {
|
|
2105
|
-
conditions.push(`n.updated_at <= $${idx++}`);
|
|
2106
|
-
params.push(criteria.updatedBefore);
|
|
2107
|
-
}
|
|
2108
|
-
if (criteria.query?.trim()) {
|
|
2109
|
-
conditions.push(`(n.tsv @@ plainto_tsquery('english', $${idx}) OR to_tsvector('english', coalesce(c.content, '')) @@ plainto_tsquery('english', $${idx}))`);
|
|
2110
|
-
params.push(criteria.query);
|
|
2111
|
-
}
|
|
2112
|
-
const result = await this.db.query(
|
|
2113
|
-
`SELECT e.note_id, e.embedding_set_id, e.id as embedding_id, e.vector::text as vector, e.created_at
|
|
2114
|
-
FROM embedding e
|
|
2115
|
-
JOIN note n ON n.id = e.note_id
|
|
2116
|
-
LEFT JOIN note_revised_current c ON c.note_id = n.id
|
|
2117
|
-
WHERE ${conditions.join(" AND ")}
|
|
2118
|
-
ORDER BY e.note_id, e.created_at DESC`,
|
|
2119
|
-
params
|
|
2120
|
-
);
|
|
2121
|
-
return result.rows;
|
|
2122
|
-
}
|
|
2123
|
-
async resolveSetOperationSource(source, compatibility, errors) {
|
|
2124
|
-
const bySet = /* @__PURE__ */ new Map();
|
|
2125
|
-
for (const setId of source.setIds) bySet.set(setId, await this.resolvePhysicalRows(setId));
|
|
2126
|
-
await this.validateCompatibility(source.setIds, compatibility, errors);
|
|
2127
|
-
const noteSets = source.setIds.map((setId) => new Set((bySet.get(setId) ?? []).map((row) => row.note_id)));
|
|
2128
|
-
const firstRows = bySet.get(source.setIds[0]) ?? [];
|
|
2129
|
-
if (source.operation === "difference") {
|
|
2130
|
-
const excluded = new Set(noteSets.slice(1).flatMap((set) => Array.from(set)));
|
|
2131
|
-
return firstRows.filter((row) => !excluded.has(row.note_id));
|
|
2132
|
-
}
|
|
2133
|
-
if (source.operation === "intersection") {
|
|
2134
|
-
return firstRows.filter((row) => noteSets.every((set) => set.has(row.note_id)));
|
|
2135
|
-
}
|
|
2136
|
-
return this.resolveDuplicateRows(source.setIds.flatMap((setId) => bySet.get(setId) ?? []), compatibility, errors);
|
|
2137
|
-
}
|
|
2138
|
-
async resolveFallbackSource(setIds, compatibility, errors) {
|
|
2139
|
-
await this.validateCompatibility(setIds, compatibility, errors);
|
|
2140
|
-
const selected = /* @__PURE__ */ new Map();
|
|
2141
|
-
for (const setId of setIds) {
|
|
2142
|
-
for (const row of await this.resolvePhysicalRows(setId)) {
|
|
2143
|
-
if (!selected.has(row.note_id)) selected.set(row.note_id, row);
|
|
2144
|
-
}
|
|
1530
|
+
async resolveFallbackSource(setIds, compatibility, errors) {
|
|
1531
|
+
await this.validateCompatibility(setIds, compatibility, errors);
|
|
1532
|
+
const selected = /* @__PURE__ */ new Map();
|
|
1533
|
+
for (const setId of setIds) {
|
|
1534
|
+
for (const row of await this.resolvePhysicalRows(setId)) {
|
|
1535
|
+
if (!selected.has(row.note_id)) selected.set(row.note_id, row);
|
|
1536
|
+
}
|
|
2145
1537
|
}
|
|
2146
1538
|
return Array.from(selected.values()).sort((a, b) => a.note_id.localeCompare(b.note_id));
|
|
2147
1539
|
}
|
|
@@ -2593,21 +1985,1032 @@ var SearchRepository = class {
|
|
|
2593
1985
|
collections: collResult.rows.map((r) => ({ id: r.id, name: r.name, count: parseInt(r.count, 10) }))
|
|
2594
1986
|
};
|
|
2595
1987
|
}
|
|
2596
|
-
async fetchTagMap(noteIds) {
|
|
2597
|
-
const tagMap = /* @__PURE__ */ new Map();
|
|
2598
|
-
if (noteIds.length === 0) return tagMap;
|
|
2599
|
-
const tagsResult = await this.db.query(
|
|
2600
|
-
`SELECT note_id, tag FROM note_tag WHERE note_id = ANY($1) ORDER BY tag`,
|
|
2601
|
-
[noteIds]
|
|
2602
|
-
);
|
|
2603
|
-
for (const row of tagsResult.rows) {
|
|
2604
|
-
const existing = tagMap.get(row.note_id) ?? [];
|
|
2605
|
-
existing.push(row.tag);
|
|
2606
|
-
tagMap.set(row.note_id, existing);
|
|
1988
|
+
async fetchTagMap(noteIds) {
|
|
1989
|
+
const tagMap = /* @__PURE__ */ new Map();
|
|
1990
|
+
if (noteIds.length === 0) return tagMap;
|
|
1991
|
+
const tagsResult = await this.db.query(
|
|
1992
|
+
`SELECT note_id, tag FROM note_tag WHERE note_id = ANY($1) ORDER BY tag`,
|
|
1993
|
+
[noteIds]
|
|
1994
|
+
);
|
|
1995
|
+
for (const row of tagsResult.rows) {
|
|
1996
|
+
const existing = tagMap.get(row.note_id) ?? [];
|
|
1997
|
+
existing.push(row.tag);
|
|
1998
|
+
tagMap.set(row.note_id, existing);
|
|
1999
|
+
}
|
|
2000
|
+
return tagMap;
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
var CaptureKnowledgeInputSchema = z.object({
|
|
2004
|
+
action: z.enum(["create", "bulk_create", "from_template"]),
|
|
2005
|
+
// For create
|
|
2006
|
+
content: z.string().optional(),
|
|
2007
|
+
title: z.string().optional(),
|
|
2008
|
+
format: z.enum(["markdown", "plain", "html"]).default("markdown"),
|
|
2009
|
+
source: z.string().default("user"),
|
|
2010
|
+
visibility: z.enum(["private", "shared", "public"]).default("private"),
|
|
2011
|
+
tags: z.array(z.string()).optional(),
|
|
2012
|
+
archive_id: z.string().optional(),
|
|
2013
|
+
// For bulk_create
|
|
2014
|
+
notes: z.array(
|
|
2015
|
+
z.object({
|
|
2016
|
+
content: z.string(),
|
|
2017
|
+
title: z.string().optional(),
|
|
2018
|
+
format: z.enum(["markdown", "plain", "html"]).default("markdown"),
|
|
2019
|
+
tags: z.array(z.string()).optional()
|
|
2020
|
+
})
|
|
2021
|
+
).optional(),
|
|
2022
|
+
// For from_template
|
|
2023
|
+
template: z.string().optional(),
|
|
2024
|
+
variables: z.record(z.string()).optional()
|
|
2025
|
+
});
|
|
2026
|
+
var ManageNoteInputSchema = z.object({
|
|
2027
|
+
action: z.enum(["update", "delete", "restore", "archive", "unarchive", "star", "unstar"]),
|
|
2028
|
+
note_id: z.string(),
|
|
2029
|
+
// For update
|
|
2030
|
+
title: z.string().optional(),
|
|
2031
|
+
content: z.string().optional(),
|
|
2032
|
+
format: z.string().optional(),
|
|
2033
|
+
visibility: z.string().optional()
|
|
2034
|
+
});
|
|
2035
|
+
var SearchInputSchema = z.object({
|
|
2036
|
+
query: z.string(),
|
|
2037
|
+
mode: z.enum(["text", "semantic", "hybrid"]).default("text"),
|
|
2038
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
2039
|
+
offset: z.number().int().min(0).default(0),
|
|
2040
|
+
tags: z.array(z.string()).optional(),
|
|
2041
|
+
collection_id: z.string().optional(),
|
|
2042
|
+
date_from: z.coerce.date().optional(),
|
|
2043
|
+
date_to: z.coerce.date().optional(),
|
|
2044
|
+
is_starred: z.boolean().optional(),
|
|
2045
|
+
is_archived: z.boolean().optional(),
|
|
2046
|
+
format: z.enum(["markdown", "plain", "html"]).optional(),
|
|
2047
|
+
source: z.string().optional(),
|
|
2048
|
+
visibility: z.enum(["private", "shared", "public"]).optional(),
|
|
2049
|
+
include_facets: z.boolean().default(false)
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
// src/tools/manage-note.ts
|
|
2053
|
+
async function manageNote(db, rawInput, events) {
|
|
2054
|
+
const input = ManageNoteInputSchema.parse(rawInput);
|
|
2055
|
+
const repo = new NotesRepository(db, events);
|
|
2056
|
+
switch (input.action) {
|
|
2057
|
+
case "update": {
|
|
2058
|
+
const note = await repo.update(input.note_id, {
|
|
2059
|
+
title: input.title,
|
|
2060
|
+
content: input.content,
|
|
2061
|
+
format: input.format,
|
|
2062
|
+
visibility: input.visibility
|
|
2063
|
+
});
|
|
2064
|
+
return { action: "update", note_id: input.note_id, note };
|
|
2065
|
+
}
|
|
2066
|
+
case "delete": {
|
|
2067
|
+
await repo.delete(input.note_id);
|
|
2068
|
+
return { action: "delete", note_id: input.note_id };
|
|
2069
|
+
}
|
|
2070
|
+
case "restore": {
|
|
2071
|
+
const note = await repo.restore(input.note_id);
|
|
2072
|
+
return { action: "restore", note_id: input.note_id, note };
|
|
2073
|
+
}
|
|
2074
|
+
case "archive": {
|
|
2075
|
+
await repo.archive(input.note_id, true);
|
|
2076
|
+
const note = await repo.get(input.note_id);
|
|
2077
|
+
return { action: "archive", note_id: input.note_id, note };
|
|
2078
|
+
}
|
|
2079
|
+
case "unarchive": {
|
|
2080
|
+
await repo.archive(input.note_id, false);
|
|
2081
|
+
const note = await repo.get(input.note_id);
|
|
2082
|
+
return { action: "unarchive", note_id: input.note_id, note };
|
|
2083
|
+
}
|
|
2084
|
+
case "star": {
|
|
2085
|
+
await repo.star(input.note_id, true);
|
|
2086
|
+
const note = await repo.get(input.note_id);
|
|
2087
|
+
return { action: "star", note_id: input.note_id, note };
|
|
2088
|
+
}
|
|
2089
|
+
case "unstar": {
|
|
2090
|
+
await repo.star(input.note_id, false);
|
|
2091
|
+
const note = await repo.get(input.note_id);
|
|
2092
|
+
return { action: "unstar", note_id: input.note_id, note };
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// src/data-backend.ts
|
|
2098
|
+
var SEMANTIC_RANK = {
|
|
2099
|
+
none: 0,
|
|
2100
|
+
"cosine-small": 1,
|
|
2101
|
+
"ann-full": 2,
|
|
2102
|
+
server: 3
|
|
2103
|
+
};
|
|
2104
|
+
var STARTUP_RANK = {
|
|
2105
|
+
instant: 0,
|
|
2106
|
+
"index-build": 1,
|
|
2107
|
+
network: 2
|
|
2108
|
+
};
|
|
2109
|
+
function missingFor(request, caps) {
|
|
2110
|
+
const missing = [];
|
|
2111
|
+
if (request.read && !caps.read) missing.push("read");
|
|
2112
|
+
if (request.write && !caps.write) missing.push("write");
|
|
2113
|
+
if (request.merge && !caps.merge) missing.push("merge");
|
|
2114
|
+
if (request.multiUser && !caps.multiUser) missing.push("multiUser");
|
|
2115
|
+
if (request.semantic && SEMANTIC_RANK[caps.semantic] < SEMANTIC_RANK[request.semantic]) {
|
|
2116
|
+
missing.push(`semantic:${request.semantic}`);
|
|
2117
|
+
}
|
|
2118
|
+
return missing;
|
|
2119
|
+
}
|
|
2120
|
+
function selectBackend(request, available) {
|
|
2121
|
+
const candidates = available.map((backend) => ({
|
|
2122
|
+
backend,
|
|
2123
|
+
missing: missingFor(request, backend.capabilities)
|
|
2124
|
+
}));
|
|
2125
|
+
if (candidates.length === 0) {
|
|
2126
|
+
return { backend: null, capabilities: null, missing: [], candidates };
|
|
2127
|
+
}
|
|
2128
|
+
const lighter = (a, b) => STARTUP_RANK[a.capabilities.startupCost] - STARTUP_RANK[b.capabilities.startupCost];
|
|
2129
|
+
const fullySatisfying = candidates.filter((c) => c.missing.length === 0);
|
|
2130
|
+
const pool = fullySatisfying.length > 0 ? fullySatisfying : candidates;
|
|
2131
|
+
const chosen = [...pool].sort((a, b) => {
|
|
2132
|
+
if (a.missing.length !== b.missing.length) return a.missing.length - b.missing.length;
|
|
2133
|
+
return lighter(a.backend, b.backend);
|
|
2134
|
+
})[0];
|
|
2135
|
+
return {
|
|
2136
|
+
backend: chosen.backend,
|
|
2137
|
+
capabilities: chosen.backend.capabilities,
|
|
2138
|
+
missing: chosen.missing,
|
|
2139
|
+
candidates
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
function toIso(d) {
|
|
2143
|
+
return d instanceof Date ? d.toISOString() : String(d);
|
|
2144
|
+
}
|
|
2145
|
+
function summaryToBackend(s) {
|
|
2146
|
+
return {
|
|
2147
|
+
id: s.id,
|
|
2148
|
+
title: s.title,
|
|
2149
|
+
tags: s.tags,
|
|
2150
|
+
createdAt: toIso(s.created_at),
|
|
2151
|
+
updatedAt: toIso(s.updated_at),
|
|
2152
|
+
source: s.source,
|
|
2153
|
+
starred: s.is_starred,
|
|
2154
|
+
archived: s.is_archived
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
function searchResultToBackend(r) {
|
|
2158
|
+
return {
|
|
2159
|
+
id: r.id,
|
|
2160
|
+
title: r.title,
|
|
2161
|
+
tags: r.tags,
|
|
2162
|
+
createdAt: toIso(r.created_at),
|
|
2163
|
+
updatedAt: toIso(r.updated_at)
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
function createPGliteBackend(db, options = {}) {
|
|
2167
|
+
const semanticAvailable = options.semanticAvailable ?? false;
|
|
2168
|
+
const notes = new NotesRepository(db);
|
|
2169
|
+
const search = new SearchRepository(db, semanticAvailable);
|
|
2170
|
+
return {
|
|
2171
|
+
id: options.id ?? "pglite",
|
|
2172
|
+
capabilities: {
|
|
2173
|
+
read: true,
|
|
2174
|
+
write: true,
|
|
2175
|
+
merge: true,
|
|
2176
|
+
multiUser: false,
|
|
2177
|
+
semantic: semanticAvailable ? "ann-full" : "none",
|
|
2178
|
+
startupCost: "index-build"
|
|
2179
|
+
},
|
|
2180
|
+
async listNotes(o) {
|
|
2181
|
+
const r = await notes.list({ offset: o?.offset, limit: o?.limit });
|
|
2182
|
+
return { items: r.items.map(summaryToBackend), total: r.total };
|
|
2183
|
+
},
|
|
2184
|
+
async getNote(id) {
|
|
2185
|
+
try {
|
|
2186
|
+
const f = await notes.get(id);
|
|
2187
|
+
return summaryToBackend(f);
|
|
2188
|
+
} catch {
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
},
|
|
2192
|
+
async search(query, o) {
|
|
2193
|
+
const r = await search.search(query, {
|
|
2194
|
+
limit: o?.limit,
|
|
2195
|
+
offset: o?.offset,
|
|
2196
|
+
tags: o?.tags,
|
|
2197
|
+
source: o?.source?.[0],
|
|
2198
|
+
include_facets: true
|
|
2199
|
+
});
|
|
2200
|
+
const hits = r.results.map((res) => ({
|
|
2201
|
+
note: searchResultToBackend(res),
|
|
2202
|
+
rank: res.rank,
|
|
2203
|
+
snippet: res.snippet
|
|
2204
|
+
}));
|
|
2205
|
+
const facets = r.facets ? {
|
|
2206
|
+
tags: Object.fromEntries(r.facets.tags.map((t) => [t.tag, t.count]))
|
|
2207
|
+
} : void 0;
|
|
2208
|
+
return { hits, total: r.total, facets };
|
|
2209
|
+
},
|
|
2210
|
+
async getNoteFull(id) {
|
|
2211
|
+
try {
|
|
2212
|
+
const f = await notes.get(id);
|
|
2213
|
+
return { ...summaryToBackend(f), content: f.current.content };
|
|
2214
|
+
} catch {
|
|
2215
|
+
return null;
|
|
2216
|
+
}
|
|
2217
|
+
},
|
|
2218
|
+
async manageNote(input) {
|
|
2219
|
+
return manageNote(db, input);
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
function shardNoteToBackend(n) {
|
|
2224
|
+
return {
|
|
2225
|
+
id: n.id,
|
|
2226
|
+
title: n.title,
|
|
2227
|
+
tags: n.tags,
|
|
2228
|
+
createdAt: toIso(n.created_at),
|
|
2229
|
+
updatedAt: toIso(n.updated_at),
|
|
2230
|
+
source: n.source,
|
|
2231
|
+
starred: n.is_starred,
|
|
2232
|
+
archived: n.is_archived
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
function createShardBackend(reader, options = {}) {
|
|
2236
|
+
const semantic = options.semantic ?? "none";
|
|
2237
|
+
return {
|
|
2238
|
+
id: options.id ?? "static-file",
|
|
2239
|
+
capabilities: {
|
|
2240
|
+
read: true,
|
|
2241
|
+
write: false,
|
|
2242
|
+
merge: false,
|
|
2243
|
+
multiUser: false,
|
|
2244
|
+
semantic,
|
|
2245
|
+
startupCost: "instant"
|
|
2246
|
+
},
|
|
2247
|
+
async listNotes(o) {
|
|
2248
|
+
const r = await reader.listNotes(o);
|
|
2249
|
+
return { items: r.items.map(shardNoteToBackend), total: r.total };
|
|
2250
|
+
},
|
|
2251
|
+
async getNote(id) {
|
|
2252
|
+
const n = await reader.getNote(id);
|
|
2253
|
+
return n ? shardNoteToBackend(n) : null;
|
|
2254
|
+
},
|
|
2255
|
+
async search(query, o) {
|
|
2256
|
+
const r = await reader.search(query, { ...o, rank: true, snippets: true });
|
|
2257
|
+
const hits = r.rankedItems ? r.rankedItems.map((it) => ({
|
|
2258
|
+
note: shardNoteToBackend(it.note),
|
|
2259
|
+
rank: it.rank,
|
|
2260
|
+
snippet: it.snippet
|
|
2261
|
+
})) : r.items.map((note) => ({ note: shardNoteToBackend(note) }));
|
|
2262
|
+
return { hits, total: r.total, facets: r.facets };
|
|
2263
|
+
},
|
|
2264
|
+
async getNoteFull(id) {
|
|
2265
|
+
const f = await reader.getNoteFull(id);
|
|
2266
|
+
if (!f) return null;
|
|
2267
|
+
return {
|
|
2268
|
+
...shardNoteToBackend(f.note),
|
|
2269
|
+
content: f.note.revised_content ?? f.note.original_content
|
|
2270
|
+
};
|
|
2271
|
+
},
|
|
2272
|
+
async semantic(query, k) {
|
|
2273
|
+
const r = await reader.semantic(query, k);
|
|
2274
|
+
return r.map(({ note, score }) => ({ note: shardNoteToBackend(note), rank: score }));
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/worker/worker-client.ts
|
|
2280
|
+
var PGliteWorkerClient = class {
|
|
2281
|
+
constructor(worker) {
|
|
2282
|
+
this.worker = worker;
|
|
2283
|
+
this.readyPromise = new Promise((resolve) => {
|
|
2284
|
+
this.resolveReady = resolve;
|
|
2285
|
+
});
|
|
2286
|
+
this.worker.addEventListener("message", (e) => {
|
|
2287
|
+
const msg = e.data;
|
|
2288
|
+
if (msg.type === "READY") {
|
|
2289
|
+
this.resolveReady();
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if (!("id" in msg)) return;
|
|
2293
|
+
const pending = this.pending.get(msg.id);
|
|
2294
|
+
if (!pending) return;
|
|
2295
|
+
this.pending.delete(msg.id);
|
|
2296
|
+
if (msg.type === "ERROR") {
|
|
2297
|
+
pending.reject(new Error(msg.error));
|
|
2298
|
+
} else {
|
|
2299
|
+
pending.resolve(msg);
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
pending = /* @__PURE__ */ new Map();
|
|
2304
|
+
readyPromise;
|
|
2305
|
+
resolveReady;
|
|
2306
|
+
/** Resolves when the worker broadcasts READY after database initialisation. */
|
|
2307
|
+
async waitReady() {
|
|
2308
|
+
return this.readyPromise;
|
|
2309
|
+
}
|
|
2310
|
+
send(request) {
|
|
2311
|
+
const id = generateId();
|
|
2312
|
+
return new Promise((resolve, reject) => {
|
|
2313
|
+
this.pending.set(id, {
|
|
2314
|
+
resolve,
|
|
2315
|
+
reject
|
|
2316
|
+
});
|
|
2317
|
+
this.worker.postMessage({ ...request, id });
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
async query(sql, params) {
|
|
2321
|
+
const resp = await this.send({ type: "QUERY", sql, params });
|
|
2322
|
+
return { rows: resp.rows, fields: resp.fields };
|
|
2323
|
+
}
|
|
2324
|
+
async exec(sql) {
|
|
2325
|
+
await this.send({ type: "EXEC", sql });
|
|
2326
|
+
}
|
|
2327
|
+
async transaction(fn) {
|
|
2328
|
+
const resp = await this.send({ type: "BEGIN" });
|
|
2329
|
+
const txId = resp.txId;
|
|
2330
|
+
const proxy = new TransactionProxy(this, txId);
|
|
2331
|
+
try {
|
|
2332
|
+
const result = await fn(proxy);
|
|
2333
|
+
await this.send({ type: "COMMIT", txId });
|
|
2334
|
+
return result;
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
await this.send({ type: "ROLLBACK", txId }).catch(() => {
|
|
2337
|
+
});
|
|
2338
|
+
throw err;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
/** Forward TX_QUERY for TransactionProxy — not part of the public surface. */
|
|
2342
|
+
async _txQuery(txId, sql, params) {
|
|
2343
|
+
const resp = await this.send({
|
|
2344
|
+
type: "TX_QUERY",
|
|
2345
|
+
txId,
|
|
2346
|
+
sql,
|
|
2347
|
+
params
|
|
2348
|
+
});
|
|
2349
|
+
return { rows: resp.rows };
|
|
2350
|
+
}
|
|
2351
|
+
/** Forward TX_EXEC for TransactionProxy — not part of the public surface. */
|
|
2352
|
+
async _txExec(txId, sql) {
|
|
2353
|
+
await this.send({ type: "TX_EXEC", txId, sql });
|
|
2354
|
+
}
|
|
2355
|
+
async ping() {
|
|
2356
|
+
await this.send({ type: "PING" });
|
|
2357
|
+
}
|
|
2358
|
+
async close() {
|
|
2359
|
+
await this.send({ type: "CLOSE" });
|
|
2360
|
+
this.worker.terminate();
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
var TransactionProxy = class {
|
|
2364
|
+
constructor(client, txId) {
|
|
2365
|
+
this.client = client;
|
|
2366
|
+
this.txId = txId;
|
|
2367
|
+
}
|
|
2368
|
+
async query(sql, params) {
|
|
2369
|
+
return this.client._txQuery(this.txId, sql, params);
|
|
2370
|
+
}
|
|
2371
|
+
async exec(sql) {
|
|
2372
|
+
return this.client._txExec(this.txId, sql);
|
|
2373
|
+
}
|
|
2374
|
+
};
|
|
2375
|
+
|
|
2376
|
+
// src/storage-backend.ts
|
|
2377
|
+
var PGliteStorageBackend = class {
|
|
2378
|
+
constructor(id, db) {
|
|
2379
|
+
this.id = id;
|
|
2380
|
+
this.db = db;
|
|
2381
|
+
}
|
|
2382
|
+
mode = "readwrite";
|
|
2383
|
+
query(sql, params) {
|
|
2384
|
+
return this.db.query(sql, params);
|
|
2385
|
+
}
|
|
2386
|
+
exec(sql) {
|
|
2387
|
+
return this.db.exec(sql);
|
|
2388
|
+
}
|
|
2389
|
+
async transaction(fn) {
|
|
2390
|
+
return this.db.transaction((tx) => fn(tx));
|
|
2391
|
+
}
|
|
2392
|
+
close() {
|
|
2393
|
+
return this.db.close();
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
var PGliteStorageBackendFactory = class {
|
|
2397
|
+
async open(input) {
|
|
2398
|
+
const db = await createPGliteInstance(input.persistence, input.archiveName);
|
|
2399
|
+
return new PGliteStorageBackend(`pglite:${input.persistence}:${input.archiveName}`, db);
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
var defaultStorageBackendFactory = new PGliteStorageBackendFactory();
|
|
2403
|
+
var PGliteWorkerStorageBackend = class {
|
|
2404
|
+
constructor(id, client) {
|
|
2405
|
+
this.id = id;
|
|
2406
|
+
this.client = client;
|
|
2407
|
+
}
|
|
2408
|
+
mode = "readwrite";
|
|
2409
|
+
query(sql, params) {
|
|
2410
|
+
return this.client.query(sql, params);
|
|
2411
|
+
}
|
|
2412
|
+
exec(sql) {
|
|
2413
|
+
return this.client.exec(sql);
|
|
2414
|
+
}
|
|
2415
|
+
transaction(fn) {
|
|
2416
|
+
return this.client.transaction(fn);
|
|
2417
|
+
}
|
|
2418
|
+
close() {
|
|
2419
|
+
return this.client.close();
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
var PGliteWorkerStorageBackendFactory = class {
|
|
2423
|
+
constructor(options) {
|
|
2424
|
+
this.options = options;
|
|
2425
|
+
}
|
|
2426
|
+
async open(input) {
|
|
2427
|
+
const worker = this.options.createWorker();
|
|
2428
|
+
const client = new PGliteWorkerClient(worker);
|
|
2429
|
+
worker.postMessage({
|
|
2430
|
+
type: "INIT",
|
|
2431
|
+
persistence: input.persistence,
|
|
2432
|
+
archiveName: input.archiveName
|
|
2433
|
+
});
|
|
2434
|
+
await client.waitReady();
|
|
2435
|
+
return new PGliteWorkerStorageBackend(`pglite-worker:${input.persistence}:${input.archiveName}`, client);
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
// src/capability-manager.ts
|
|
2440
|
+
var VALID_TRANSITIONS = {
|
|
2441
|
+
enable: ["unloaded", "disabled", "error"],
|
|
2442
|
+
disable: ["ready"],
|
|
2443
|
+
markReady: ["loading"],
|
|
2444
|
+
markError: ["loading"]
|
|
2445
|
+
};
|
|
2446
|
+
function assertTransition(method, name, current) {
|
|
2447
|
+
const allowed = VALID_TRANSITIONS[method];
|
|
2448
|
+
if (!allowed.includes(current)) {
|
|
2449
|
+
throw new Error(
|
|
2450
|
+
`CapabilityManager.${method}('${name}'): invalid transition from state '${current}'. Allowed source states: ${allowed.join(", ")}.`
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
var CapabilityManager = class {
|
|
2455
|
+
constructor(events) {
|
|
2456
|
+
this.events = events;
|
|
2457
|
+
const names = ["semantic", "llm", "audio", "vision", "pdf"];
|
|
2458
|
+
for (const name of names) {
|
|
2459
|
+
this.capabilities.set(name, { name, state: "unloaded" });
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
capabilities = /* @__PURE__ */ new Map();
|
|
2463
|
+
loaders = /* @__PURE__ */ new Map();
|
|
2464
|
+
progressMessages = /* @__PURE__ */ new Map();
|
|
2465
|
+
/**
|
|
2466
|
+
* Register an async loader for a capability.
|
|
2467
|
+
* Called by enable(); if no loader is registered the capability transitions
|
|
2468
|
+
* directly to ready (useful for capabilities that require no async init).
|
|
2469
|
+
*/
|
|
2470
|
+
registerLoader(name, loader) {
|
|
2471
|
+
this.loaders.set(name, loader);
|
|
2472
|
+
}
|
|
2473
|
+
getState(name) {
|
|
2474
|
+
return this.capabilities.get(name)?.state ?? "unloaded";
|
|
2475
|
+
}
|
|
2476
|
+
isReady(name) {
|
|
2477
|
+
return this.getState(name) === "ready";
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Enable a capability.
|
|
2481
|
+
* Valid from: unloaded, disabled, error (retry).
|
|
2482
|
+
* Runs the registered loader if present; transitions to ready on success,
|
|
2483
|
+
* error on failure.
|
|
2484
|
+
*/
|
|
2485
|
+
async enable(name) {
|
|
2486
|
+
const entry = this.capabilities.get(name);
|
|
2487
|
+
if (!entry) return;
|
|
2488
|
+
if (entry.state === "ready" || entry.state === "loading") return;
|
|
2489
|
+
assertTransition("enable", name, entry.state);
|
|
2490
|
+
entry.state = "loading";
|
|
2491
|
+
entry.error = void 0;
|
|
2492
|
+
this.events.emit("capability.loading", { name });
|
|
2493
|
+
const loader = this.loaders.get(name);
|
|
2494
|
+
if (!loader) {
|
|
2495
|
+
entry.state = "ready";
|
|
2496
|
+
this.events.emit("capability.ready", { name });
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
try {
|
|
2500
|
+
await loader();
|
|
2501
|
+
if (entry.state === "loading") {
|
|
2502
|
+
entry.state = "ready";
|
|
2503
|
+
this.events.emit("capability.ready", { name });
|
|
2504
|
+
}
|
|
2505
|
+
} catch (err) {
|
|
2506
|
+
if (entry.state === "loading") {
|
|
2507
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2508
|
+
entry.state = "error";
|
|
2509
|
+
entry.error = message;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Disable a ready capability.
|
|
2515
|
+
* Valid from: ready only.
|
|
2516
|
+
*/
|
|
2517
|
+
disable(name) {
|
|
2518
|
+
const entry = this.capabilities.get(name);
|
|
2519
|
+
if (!entry) return;
|
|
2520
|
+
assertTransition("disable", name, entry.state);
|
|
2521
|
+
entry.state = "disabled";
|
|
2522
|
+
this.events.emit("capability.disabled", { name });
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Mark a loading capability as ready (external use, e.g. bridge protocol).
|
|
2526
|
+
* Valid from: loading only.
|
|
2527
|
+
*/
|
|
2528
|
+
markReady(name) {
|
|
2529
|
+
const entry = this.capabilities.get(name);
|
|
2530
|
+
if (!entry) return;
|
|
2531
|
+
assertTransition("markReady", name, entry.state);
|
|
2532
|
+
entry.state = "ready";
|
|
2533
|
+
entry.error = void 0;
|
|
2534
|
+
this.events.emit("capability.ready", { name });
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Mark a loading capability as errored (external use, e.g. bridge protocol).
|
|
2538
|
+
* Valid from: loading only.
|
|
2539
|
+
*/
|
|
2540
|
+
markError(name, error) {
|
|
2541
|
+
const entry = this.capabilities.get(name);
|
|
2542
|
+
if (!entry) return;
|
|
2543
|
+
assertTransition("markError", name, entry.state);
|
|
2544
|
+
entry.state = "error";
|
|
2545
|
+
entry.error = error;
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Report loading progress (0-100).
|
|
2549
|
+
* Emits capability.loading with progress if the capability is currently loading.
|
|
2550
|
+
* No-op if the capability is not in loading state.
|
|
2551
|
+
*/
|
|
2552
|
+
reportProgress(name, progress) {
|
|
2553
|
+
const entry = this.capabilities.get(name);
|
|
2554
|
+
if (!entry || entry.state !== "loading") return;
|
|
2555
|
+
this.events.emit("capability.loading", { name, progress });
|
|
2556
|
+
}
|
|
2557
|
+
/** Set a human-readable progress message for a loading capability */
|
|
2558
|
+
setProgress(name, message) {
|
|
2559
|
+
this.progressMessages.set(name, message);
|
|
2560
|
+
this.events.emit("capability.loading", { name, progress: -1 });
|
|
2561
|
+
}
|
|
2562
|
+
/** Get the current progress message for a capability */
|
|
2563
|
+
getProgress(name) {
|
|
2564
|
+
return this.progressMessages.get(name);
|
|
2565
|
+
}
|
|
2566
|
+
getError(name) {
|
|
2567
|
+
return this.capabilities.get(name)?.error;
|
|
2568
|
+
}
|
|
2569
|
+
listAll() {
|
|
2570
|
+
return Array.from(this.capabilities.values()).map(({ name, state }) => ({
|
|
2571
|
+
name,
|
|
2572
|
+
state
|
|
2573
|
+
}));
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
|
|
2577
|
+
// src/migration-runner.ts
|
|
2578
|
+
var MigrationRunner = class {
|
|
2579
|
+
constructor(db, events) {
|
|
2580
|
+
this.db = db;
|
|
2581
|
+
this.events = events;
|
|
2582
|
+
}
|
|
2583
|
+
async ensureSchemaTable() {
|
|
2584
|
+
await this.db.exec(`
|
|
2585
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
2586
|
+
version INTEGER NOT NULL,
|
|
2587
|
+
name TEXT NOT NULL,
|
|
2588
|
+
applied_at TIMESTAMPTZ DEFAULT now(),
|
|
2589
|
+
PRIMARY KEY (version)
|
|
2590
|
+
)
|
|
2591
|
+
`);
|
|
2592
|
+
}
|
|
2593
|
+
async getCurrentVersion() {
|
|
2594
|
+
const result = await this.db.query(
|
|
2595
|
+
"SELECT COALESCE(MAX(version), 0) AS version FROM schema_version"
|
|
2596
|
+
);
|
|
2597
|
+
return result.rows[0].version;
|
|
2598
|
+
}
|
|
2599
|
+
async apply(migrations) {
|
|
2600
|
+
await this.ensureSchemaTable();
|
|
2601
|
+
const currentVersion = await this.getCurrentVersion();
|
|
2602
|
+
const pending = migrations.filter((m) => m.version > currentVersion).sort((a, b) => a.version - b.version);
|
|
2603
|
+
let applied = 0;
|
|
2604
|
+
for (const migration of pending) {
|
|
2605
|
+
await this.db.transaction(async (tx) => {
|
|
2606
|
+
await tx.exec(migration.sql);
|
|
2607
|
+
await tx.query(
|
|
2608
|
+
"INSERT INTO schema_version (version, name) VALUES ($1, $2)",
|
|
2609
|
+
[migration.version, migration.name]
|
|
2610
|
+
);
|
|
2611
|
+
});
|
|
2612
|
+
applied++;
|
|
2613
|
+
this.events?.emit("migration.applied", { version: migration.version });
|
|
2614
|
+
}
|
|
2615
|
+
return applied;
|
|
2616
|
+
}
|
|
2617
|
+
async getAppliedMigrations() {
|
|
2618
|
+
const result = await this.db.query(
|
|
2619
|
+
"SELECT version, name FROM schema_version ORDER BY version"
|
|
2620
|
+
);
|
|
2621
|
+
return result.rows;
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
|
|
2625
|
+
// src/archive-manager.ts
|
|
2626
|
+
var ArchiveManager = class {
|
|
2627
|
+
constructor(persistenceOrFactory, events, persistenceOverride) {
|
|
2628
|
+
this.events = events;
|
|
2629
|
+
if (typeof persistenceOrFactory === "string") {
|
|
2630
|
+
this.persistence = persistenceOrFactory;
|
|
2631
|
+
this.backendFactory = defaultStorageBackendFactory;
|
|
2632
|
+
} else {
|
|
2633
|
+
this.persistence = persistenceOverride ?? "memory";
|
|
2634
|
+
this.backendFactory = persistenceOrFactory;
|
|
2635
|
+
}
|
|
2636
|
+
this.archives.set("default", {
|
|
2637
|
+
name: "default",
|
|
2638
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
currentArchive = "default";
|
|
2642
|
+
db = null;
|
|
2643
|
+
archives = /* @__PURE__ */ new Map();
|
|
2644
|
+
persistence;
|
|
2645
|
+
backendFactory;
|
|
2646
|
+
getCurrentArchiveName() {
|
|
2647
|
+
return this.currentArchive;
|
|
2648
|
+
}
|
|
2649
|
+
getDb() {
|
|
2650
|
+
return this.db;
|
|
2651
|
+
}
|
|
2652
|
+
async open(archiveName = "default") {
|
|
2653
|
+
if (this.db) {
|
|
2654
|
+
await this.db.close();
|
|
2655
|
+
}
|
|
2656
|
+
this.db = await this.backendFactory.open({
|
|
2657
|
+
archiveName,
|
|
2658
|
+
persistence: this.persistence
|
|
2659
|
+
});
|
|
2660
|
+
const runner = new MigrationRunner(this.db, this.events);
|
|
2661
|
+
await runner.apply(allMigrations);
|
|
2662
|
+
this.currentArchive = archiveName;
|
|
2663
|
+
if (!this.archives.has(archiveName)) {
|
|
2664
|
+
this.archives.set(archiveName, {
|
|
2665
|
+
name: archiveName,
|
|
2666
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
this.events?.emit("archive.switched", { name: archiveName });
|
|
2670
|
+
return this.db;
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Adopt an already-created backend WITHOUT running migrations — for a backend
|
|
2674
|
+
* whose schema is already present, e.g. a PGlite restored from a physical
|
|
2675
|
+
* data-dir snapshot (issue #187, `restoreDbSnapshot`). Running migrations here
|
|
2676
|
+
* would be wrong: the restored dir already carries them (and the HNSW index).
|
|
2677
|
+
*/
|
|
2678
|
+
async adopt(backend, archiveName = "default") {
|
|
2679
|
+
if (this.db) {
|
|
2680
|
+
await this.db.close();
|
|
2681
|
+
}
|
|
2682
|
+
this.db = backend;
|
|
2683
|
+
this.currentArchive = archiveName;
|
|
2684
|
+
if (!this.archives.has(archiveName)) {
|
|
2685
|
+
this.archives.set(archiveName, {
|
|
2686
|
+
name: archiveName,
|
|
2687
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
this.events?.emit("archive.switched", { name: archiveName });
|
|
2691
|
+
return this.db;
|
|
2692
|
+
}
|
|
2693
|
+
async create(archiveName) {
|
|
2694
|
+
if (this.archives.has(archiveName)) {
|
|
2695
|
+
throw new Error(`Archive '${archiveName}' already exists`);
|
|
2696
|
+
}
|
|
2697
|
+
return this.open(archiveName);
|
|
2698
|
+
}
|
|
2699
|
+
async switchTo(archiveName) {
|
|
2700
|
+
return this.open(archiveName);
|
|
2701
|
+
}
|
|
2702
|
+
async delete(archiveName) {
|
|
2703
|
+
if (archiveName === "default") {
|
|
2704
|
+
throw new Error("Cannot delete the default archive");
|
|
2705
|
+
}
|
|
2706
|
+
if (this.currentArchive === archiveName && this.db) {
|
|
2707
|
+
await this.db.close();
|
|
2708
|
+
this.db = null;
|
|
2709
|
+
}
|
|
2710
|
+
this.archives.delete(archiveName);
|
|
2711
|
+
}
|
|
2712
|
+
listArchives() {
|
|
2713
|
+
return Array.from(this.archives.values());
|
|
2714
|
+
}
|
|
2715
|
+
async close() {
|
|
2716
|
+
if (this.db) {
|
|
2717
|
+
await this.db.close();
|
|
2718
|
+
this.db = null;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
|
|
2723
|
+
// src/create-fortemi.ts
|
|
2724
|
+
function createFortemi(config) {
|
|
2725
|
+
const events = new TypedEventBus();
|
|
2726
|
+
return {
|
|
2727
|
+
events,
|
|
2728
|
+
config,
|
|
2729
|
+
destroy() {
|
|
2730
|
+
events.removeAllListeners();
|
|
2731
|
+
}
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
// src/service-worker/register.ts
|
|
2736
|
+
async function registerServiceWorker(swUrl = "/sw.js") {
|
|
2737
|
+
if (!("serviceWorker" in navigator)) {
|
|
2738
|
+
return { registered: false, error: "Service Workers not supported" };
|
|
2739
|
+
}
|
|
2740
|
+
try {
|
|
2741
|
+
const registration = await navigator.serviceWorker.register(swUrl, {
|
|
2742
|
+
type: "module",
|
|
2743
|
+
scope: "/"
|
|
2744
|
+
});
|
|
2745
|
+
if (registration.installing) {
|
|
2746
|
+
await new Promise((resolve) => {
|
|
2747
|
+
registration.installing.addEventListener("statechange", function handler() {
|
|
2748
|
+
if (this.state === "activated") {
|
|
2749
|
+
this.removeEventListener("statechange", handler);
|
|
2750
|
+
resolve();
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
return { registered: true, registration };
|
|
2756
|
+
} catch (err) {
|
|
2757
|
+
return {
|
|
2758
|
+
registered: false,
|
|
2759
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
// src/service-worker/routes.ts
|
|
2765
|
+
async function parseJsonBody(request) {
|
|
2766
|
+
try {
|
|
2767
|
+
return await request.json();
|
|
2768
|
+
} catch {
|
|
2769
|
+
return null;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
function jsonResponse(data, status = 200) {
|
|
2773
|
+
return new Response(JSON.stringify(data), {
|
|
2774
|
+
status,
|
|
2775
|
+
headers: { "Content-Type": "application/json" }
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
function errorResponse(message, status) {
|
|
2779
|
+
return jsonResponse({ error: message }, status);
|
|
2780
|
+
}
|
|
2781
|
+
var DB_NOT_CONNECTED = "Database not connected. REST API available in standalone mode only.";
|
|
2782
|
+
function createRoutes() {
|
|
2783
|
+
return [
|
|
2784
|
+
// GET /api/v1/notes — list notes
|
|
2785
|
+
{
|
|
2786
|
+
method: "GET",
|
|
2787
|
+
pattern: /^\/api\/v1\/notes\/?$/,
|
|
2788
|
+
handler: async () => {
|
|
2789
|
+
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
2790
|
+
}
|
|
2791
|
+
},
|
|
2792
|
+
// POST /api/v1/notes — create note
|
|
2793
|
+
{
|
|
2794
|
+
method: "POST",
|
|
2795
|
+
pattern: /^\/api\/v1\/notes\/?$/,
|
|
2796
|
+
handler: async (request) => {
|
|
2797
|
+
const body = await parseJsonBody(request);
|
|
2798
|
+
if (body === null) return errorResponse("Invalid JSON body", 400);
|
|
2799
|
+
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
2800
|
+
}
|
|
2801
|
+
},
|
|
2802
|
+
// POST /api/v1/notes/:id/restore — restore soft-deleted note (must come before /:id)
|
|
2803
|
+
{
|
|
2804
|
+
method: "POST",
|
|
2805
|
+
pattern: /^\/api\/v1\/notes\/([^/]+)\/restore\/?$/,
|
|
2806
|
+
handler: async () => {
|
|
2807
|
+
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
2808
|
+
}
|
|
2809
|
+
},
|
|
2810
|
+
// POST /api/v1/notes/:id/star — star or unstar a note
|
|
2811
|
+
{
|
|
2812
|
+
method: "POST",
|
|
2813
|
+
pattern: /^\/api\/v1\/notes\/([^/]+)\/star\/?$/,
|
|
2814
|
+
handler: async () => {
|
|
2815
|
+
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
2816
|
+
}
|
|
2817
|
+
},
|
|
2818
|
+
// POST /api/v1/notes/:id/archive — archive or unarchive a note
|
|
2819
|
+
{
|
|
2820
|
+
method: "POST",
|
|
2821
|
+
pattern: /^\/api\/v1\/notes\/([^/]+)\/archive\/?$/,
|
|
2822
|
+
handler: async () => {
|
|
2823
|
+
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
2824
|
+
}
|
|
2825
|
+
},
|
|
2826
|
+
// GET /api/v1/notes/:id — get a single note
|
|
2827
|
+
{
|
|
2828
|
+
method: "GET",
|
|
2829
|
+
pattern: /^\/api\/v1\/notes\/([^/]+)\/?$/,
|
|
2830
|
+
handler: async () => {
|
|
2831
|
+
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
2832
|
+
}
|
|
2833
|
+
},
|
|
2834
|
+
// PUT /api/v1/notes/:id — update a note
|
|
2835
|
+
{
|
|
2836
|
+
method: "PUT",
|
|
2837
|
+
pattern: /^\/api\/v1\/notes\/([^/]+)\/?$/,
|
|
2838
|
+
handler: async () => {
|
|
2839
|
+
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
2840
|
+
}
|
|
2841
|
+
},
|
|
2842
|
+
// DELETE /api/v1/notes/:id — soft-delete a note
|
|
2843
|
+
{
|
|
2844
|
+
method: "DELETE",
|
|
2845
|
+
pattern: /^\/api\/v1\/notes\/([^/]+)\/?$/,
|
|
2846
|
+
handler: async () => {
|
|
2847
|
+
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
2848
|
+
}
|
|
2849
|
+
},
|
|
2850
|
+
// GET /api/v1/search — full-text search
|
|
2851
|
+
{
|
|
2852
|
+
method: "GET",
|
|
2853
|
+
pattern: /^\/api\/v1\/search\/?$/,
|
|
2854
|
+
handler: async () => {
|
|
2855
|
+
return errorResponse(DB_NOT_CONNECTED, 503);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
];
|
|
2859
|
+
}
|
|
2860
|
+
function matchRoute(routes, request, url) {
|
|
2861
|
+
for (const route of routes) {
|
|
2862
|
+
if (request.method !== route.method) continue;
|
|
2863
|
+
const match = url.pathname.match(route.pattern);
|
|
2864
|
+
if (match) return route;
|
|
2865
|
+
}
|
|
2866
|
+
return null;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// src/blob-store.ts
|
|
2870
|
+
function hashPath(hash) {
|
|
2871
|
+
return {
|
|
2872
|
+
dir1: hash.slice(0, 2),
|
|
2873
|
+
dir2: hash.slice(2, 4),
|
|
2874
|
+
filename: hash
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
var OpfsBlobStore = class {
|
|
2878
|
+
constructor(archiveName) {
|
|
2879
|
+
this.archiveName = archiveName;
|
|
2880
|
+
}
|
|
2881
|
+
async getRoot() {
|
|
2882
|
+
const root = await navigator.storage.getDirectory();
|
|
2883
|
+
return root.getDirectoryHandle(`fortemi-${this.archiveName}-blobs`, { create: true });
|
|
2884
|
+
}
|
|
2885
|
+
async getFileHandle(hash, create) {
|
|
2886
|
+
const { dir1, dir2, filename } = hashPath(hash);
|
|
2887
|
+
try {
|
|
2888
|
+
const root = await this.getRoot();
|
|
2889
|
+
const d1 = await root.getDirectoryHandle(dir1, { create });
|
|
2890
|
+
const d2 = await d1.getDirectoryHandle(dir2, { create });
|
|
2891
|
+
return d2.getFileHandle(filename, { create });
|
|
2892
|
+
} catch (err) {
|
|
2893
|
+
if (err instanceof DOMException && err.name === "NotFoundError") {
|
|
2894
|
+
return null;
|
|
2895
|
+
}
|
|
2896
|
+
throw err;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
async write(hash, data) {
|
|
2900
|
+
const fh = await this.getFileHandle(hash, true);
|
|
2901
|
+
if (!fh) throw new Error(`OpfsBlobStore: could not create file for hash ${hash}`);
|
|
2902
|
+
const writable = await fh.createWritable();
|
|
2903
|
+
await writable.write(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
2904
|
+
await writable.close();
|
|
2905
|
+
}
|
|
2906
|
+
async read(hash) {
|
|
2907
|
+
const fh = await this.getFileHandle(hash, false);
|
|
2908
|
+
if (!fh) return null;
|
|
2909
|
+
const file = await fh.getFile();
|
|
2910
|
+
const buffer = await file.arrayBuffer();
|
|
2911
|
+
return new Uint8Array(buffer);
|
|
2912
|
+
}
|
|
2913
|
+
async remove(hash) {
|
|
2914
|
+
const { dir1, dir2, filename } = hashPath(hash);
|
|
2915
|
+
try {
|
|
2916
|
+
const root = await this.getRoot();
|
|
2917
|
+
const d1 = await root.getDirectoryHandle(dir1, { create: false });
|
|
2918
|
+
const d2 = await d1.getDirectoryHandle(dir2, { create: false });
|
|
2919
|
+
await d2.removeEntry(filename);
|
|
2920
|
+
} catch (err) {
|
|
2921
|
+
if (err instanceof DOMException && err.name === "NotFoundError") {
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
throw err;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
async exists(hash) {
|
|
2928
|
+
const fh = await this.getFileHandle(hash, false);
|
|
2929
|
+
return fh !== null;
|
|
2930
|
+
}
|
|
2931
|
+
};
|
|
2932
|
+
var IDB_STORE = "blobs";
|
|
2933
|
+
var IDB_VERSION = 1;
|
|
2934
|
+
function openDb(dbName) {
|
|
2935
|
+
return new Promise((resolve, reject) => {
|
|
2936
|
+
const req = indexedDB.open(dbName, IDB_VERSION);
|
|
2937
|
+
req.onupgradeneeded = () => {
|
|
2938
|
+
req.result.createObjectStore(IDB_STORE);
|
|
2939
|
+
};
|
|
2940
|
+
req.onsuccess = () => resolve(req.result);
|
|
2941
|
+
req.onerror = () => reject(req.error);
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
var IdbBlobStore = class {
|
|
2945
|
+
dbName;
|
|
2946
|
+
_db = null;
|
|
2947
|
+
constructor(archiveName) {
|
|
2948
|
+
this.dbName = `fortemi-${archiveName}-blobs`;
|
|
2949
|
+
}
|
|
2950
|
+
async db() {
|
|
2951
|
+
if (!this._db) {
|
|
2952
|
+
this._db = await openDb(this.dbName);
|
|
2607
2953
|
}
|
|
2608
|
-
return
|
|
2954
|
+
return this._db;
|
|
2955
|
+
}
|
|
2956
|
+
async write(hash, data) {
|
|
2957
|
+
const db = await this.db();
|
|
2958
|
+
return new Promise((resolve, reject) => {
|
|
2959
|
+
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
2960
|
+
tx.objectStore(IDB_STORE).put(data, hash);
|
|
2961
|
+
tx.oncomplete = () => resolve();
|
|
2962
|
+
tx.onerror = () => reject(tx.error);
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
async read(hash) {
|
|
2966
|
+
const db = await this.db();
|
|
2967
|
+
return new Promise((resolve, reject) => {
|
|
2968
|
+
const tx = db.transaction(IDB_STORE, "readonly");
|
|
2969
|
+
const req = tx.objectStore(IDB_STORE).get(hash);
|
|
2970
|
+
req.onsuccess = () => resolve(req.result ?? null);
|
|
2971
|
+
req.onerror = () => reject(req.error);
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
async remove(hash) {
|
|
2975
|
+
const db = await this.db();
|
|
2976
|
+
return new Promise((resolve, reject) => {
|
|
2977
|
+
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
2978
|
+
tx.objectStore(IDB_STORE).delete(hash);
|
|
2979
|
+
tx.oncomplete = () => resolve();
|
|
2980
|
+
tx.onerror = () => reject(tx.error);
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
async exists(hash) {
|
|
2984
|
+
const db = await this.db();
|
|
2985
|
+
return new Promise((resolve, reject) => {
|
|
2986
|
+
const tx = db.transaction(IDB_STORE, "readonly");
|
|
2987
|
+
const req = tx.objectStore(IDB_STORE).count(hash);
|
|
2988
|
+
req.onsuccess = () => resolve(req.result > 0);
|
|
2989
|
+
req.onerror = () => reject(req.error);
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
};
|
|
2993
|
+
var MemoryBlobStore = class {
|
|
2994
|
+
store = /* @__PURE__ */ new Map();
|
|
2995
|
+
async write(hash, data) {
|
|
2996
|
+
this.store.set(hash, data);
|
|
2997
|
+
}
|
|
2998
|
+
async read(hash) {
|
|
2999
|
+
return this.store.get(hash) ?? null;
|
|
3000
|
+
}
|
|
3001
|
+
async remove(hash) {
|
|
3002
|
+
this.store.delete(hash);
|
|
3003
|
+
}
|
|
3004
|
+
async exists(hash) {
|
|
3005
|
+
return this.store.has(hash);
|
|
2609
3006
|
}
|
|
2610
3007
|
};
|
|
3008
|
+
function createBlobStore(archiveName) {
|
|
3009
|
+
if (typeof navigator !== "undefined" && "storage" in navigator && "getDirectory" in navigator.storage) {
|
|
3010
|
+
return new OpfsBlobStore(archiveName);
|
|
3011
|
+
}
|
|
3012
|
+
return new IdbBlobStore(archiveName);
|
|
3013
|
+
}
|
|
2611
3014
|
|
|
2612
3015
|
// src/repositories/graph-repository.ts
|
|
2613
3016
|
var SIMILARITY_GRAPH_ALGORITHM = "knn-batched-v1";
|
|
@@ -3716,54 +4119,6 @@ var SkosRepository = class {
|
|
|
3716
4119
|
return result.rows;
|
|
3717
4120
|
}
|
|
3718
4121
|
};
|
|
3719
|
-
var CaptureKnowledgeInputSchema = z.object({
|
|
3720
|
-
action: z.enum(["create", "bulk_create", "from_template"]),
|
|
3721
|
-
// For create
|
|
3722
|
-
content: z.string().optional(),
|
|
3723
|
-
title: z.string().optional(),
|
|
3724
|
-
format: z.enum(["markdown", "plain", "html"]).default("markdown"),
|
|
3725
|
-
source: z.string().default("user"),
|
|
3726
|
-
visibility: z.enum(["private", "shared", "public"]).default("private"),
|
|
3727
|
-
tags: z.array(z.string()).optional(),
|
|
3728
|
-
archive_id: z.string().optional(),
|
|
3729
|
-
// For bulk_create
|
|
3730
|
-
notes: z.array(
|
|
3731
|
-
z.object({
|
|
3732
|
-
content: z.string(),
|
|
3733
|
-
title: z.string().optional(),
|
|
3734
|
-
format: z.enum(["markdown", "plain", "html"]).default("markdown"),
|
|
3735
|
-
tags: z.array(z.string()).optional()
|
|
3736
|
-
})
|
|
3737
|
-
).optional(),
|
|
3738
|
-
// For from_template
|
|
3739
|
-
template: z.string().optional(),
|
|
3740
|
-
variables: z.record(z.string()).optional()
|
|
3741
|
-
});
|
|
3742
|
-
var ManageNoteInputSchema = z.object({
|
|
3743
|
-
action: z.enum(["update", "delete", "restore", "archive", "unarchive", "star", "unstar"]),
|
|
3744
|
-
note_id: z.string(),
|
|
3745
|
-
// For update
|
|
3746
|
-
title: z.string().optional(),
|
|
3747
|
-
content: z.string().optional(),
|
|
3748
|
-
format: z.string().optional(),
|
|
3749
|
-
visibility: z.string().optional()
|
|
3750
|
-
});
|
|
3751
|
-
var SearchInputSchema = z.object({
|
|
3752
|
-
query: z.string(),
|
|
3753
|
-
mode: z.enum(["text", "semantic", "hybrid"]).default("text"),
|
|
3754
|
-
limit: z.number().int().min(1).max(100).default(20),
|
|
3755
|
-
offset: z.number().int().min(0).default(0),
|
|
3756
|
-
tags: z.array(z.string()).optional(),
|
|
3757
|
-
collection_id: z.string().optional(),
|
|
3758
|
-
date_from: z.coerce.date().optional(),
|
|
3759
|
-
date_to: z.coerce.date().optional(),
|
|
3760
|
-
is_starred: z.boolean().optional(),
|
|
3761
|
-
is_archived: z.boolean().optional(),
|
|
3762
|
-
format: z.enum(["markdown", "plain", "html"]).optional(),
|
|
3763
|
-
source: z.string().optional(),
|
|
3764
|
-
visibility: z.enum(["private", "shared", "public"]).optional(),
|
|
3765
|
-
include_facets: z.boolean().default(false)
|
|
3766
|
-
});
|
|
3767
4122
|
|
|
3768
4123
|
// src/tools/capture-knowledge.ts
|
|
3769
4124
|
async function captureKnowledge(db, rawInput, events) {
|
|
@@ -3816,51 +4171,6 @@ async function captureKnowledge(db, rawInput, events) {
|
|
|
3816
4171
|
}
|
|
3817
4172
|
}
|
|
3818
4173
|
|
|
3819
|
-
// src/tools/manage-note.ts
|
|
3820
|
-
async function manageNote(db, rawInput, events) {
|
|
3821
|
-
const input = ManageNoteInputSchema.parse(rawInput);
|
|
3822
|
-
const repo = new NotesRepository(db, events);
|
|
3823
|
-
switch (input.action) {
|
|
3824
|
-
case "update": {
|
|
3825
|
-
const note = await repo.update(input.note_id, {
|
|
3826
|
-
title: input.title,
|
|
3827
|
-
content: input.content,
|
|
3828
|
-
format: input.format,
|
|
3829
|
-
visibility: input.visibility
|
|
3830
|
-
});
|
|
3831
|
-
return { action: "update", note_id: input.note_id, note };
|
|
3832
|
-
}
|
|
3833
|
-
case "delete": {
|
|
3834
|
-
await repo.delete(input.note_id);
|
|
3835
|
-
return { action: "delete", note_id: input.note_id };
|
|
3836
|
-
}
|
|
3837
|
-
case "restore": {
|
|
3838
|
-
const note = await repo.restore(input.note_id);
|
|
3839
|
-
return { action: "restore", note_id: input.note_id, note };
|
|
3840
|
-
}
|
|
3841
|
-
case "archive": {
|
|
3842
|
-
await repo.archive(input.note_id, true);
|
|
3843
|
-
const note = await repo.get(input.note_id);
|
|
3844
|
-
return { action: "archive", note_id: input.note_id, note };
|
|
3845
|
-
}
|
|
3846
|
-
case "unarchive": {
|
|
3847
|
-
await repo.archive(input.note_id, false);
|
|
3848
|
-
const note = await repo.get(input.note_id);
|
|
3849
|
-
return { action: "unarchive", note_id: input.note_id, note };
|
|
3850
|
-
}
|
|
3851
|
-
case "star": {
|
|
3852
|
-
await repo.star(input.note_id, true);
|
|
3853
|
-
const note = await repo.get(input.note_id);
|
|
3854
|
-
return { action: "star", note_id: input.note_id, note };
|
|
3855
|
-
}
|
|
3856
|
-
case "unstar": {
|
|
3857
|
-
await repo.star(input.note_id, false);
|
|
3858
|
-
const note = await repo.get(input.note_id);
|
|
3859
|
-
return { action: "unstar", note_id: input.note_id, note };
|
|
3860
|
-
}
|
|
3861
|
-
}
|
|
3862
|
-
}
|
|
3863
|
-
|
|
3864
4174
|
// src/tools/search.ts
|
|
3865
4175
|
async function searchTool(db, rawInput) {
|
|
3866
4176
|
const input = SearchInputSchema.parse(rawInput);
|
|
@@ -5104,13 +5414,13 @@ var OpenAICompatibleProvider = class {
|
|
|
5104
5414
|
throw new Error("Streaming not supported: response body is null");
|
|
5105
5415
|
}
|
|
5106
5416
|
const reader = response.body.getReader();
|
|
5107
|
-
const
|
|
5417
|
+
const decoder4 = new TextDecoder();
|
|
5108
5418
|
let buffer = "";
|
|
5109
5419
|
try {
|
|
5110
5420
|
while (true) {
|
|
5111
5421
|
const { done, value } = await reader.read();
|
|
5112
5422
|
if (done) break;
|
|
5113
|
-
buffer +=
|
|
5423
|
+
buffer += decoder4.decode(value, { stream: true });
|
|
5114
5424
|
const lines = buffer.split("\n");
|
|
5115
5425
|
buffer = lines.pop() ?? "";
|
|
5116
5426
|
for (const line of lines) {
|
|
@@ -6141,8 +6451,21 @@ async function exportShard(db, options) {
|
|
|
6141
6451
|
tags: tagsByNote.get(row.id) ?? []
|
|
6142
6452
|
}));
|
|
6143
6453
|
const exportedNoteIds = new Set(notes.map((n) => n.id));
|
|
6144
|
-
const
|
|
6145
|
-
|
|
6454
|
+
const shardNotes = notes.map((n) => noteToShard(n));
|
|
6455
|
+
let layout;
|
|
6456
|
+
const clusterSize = options?.clusterNotesSize;
|
|
6457
|
+
if (clusterSize && Number.isInteger(clusterSize) && clusterSize > 0 && shardNotes.length > 0) {
|
|
6458
|
+
const clusters = [];
|
|
6459
|
+
for (let offset = 0; offset < shardNotes.length; offset += clusterSize) {
|
|
6460
|
+
const slice = shardNotes.slice(offset, offset + clusterSize);
|
|
6461
|
+
const href = `notes/${String(offset).padStart(6, "0")}.jsonl`;
|
|
6462
|
+
clusters.push({ href, offset, count: slice.length });
|
|
6463
|
+
files.set(href, encoder.encode(slice.map((n) => JSON.stringify(n)).join("\n")));
|
|
6464
|
+
}
|
|
6465
|
+
layout = { clusters: { notes: clusters } };
|
|
6466
|
+
} else {
|
|
6467
|
+
files.set("notes.jsonl", encoder.encode(shardNotes.map((n) => JSON.stringify(n)).join("\n")));
|
|
6468
|
+
}
|
|
6146
6469
|
components.push("notes");
|
|
6147
6470
|
counts.notes = notes.length;
|
|
6148
6471
|
const collectionRows = await db.query(
|
|
@@ -6375,7 +6698,8 @@ async function exportShard(db, options) {
|
|
|
6375
6698
|
components,
|
|
6376
6699
|
counts,
|
|
6377
6700
|
checksums,
|
|
6378
|
-
min_reader_version: "1.0.0"
|
|
6701
|
+
min_reader_version: "1.0.0",
|
|
6702
|
+
...layout ? { layout } : {}
|
|
6379
6703
|
};
|
|
6380
6704
|
files.set("manifest.json", encoder.encode(JSON.stringify(manifest, null, 2)));
|
|
6381
6705
|
return packTarGz(files);
|
|
@@ -6489,7 +6813,8 @@ async function importShard(db, data, options) {
|
|
|
6489
6813
|
};
|
|
6490
6814
|
}
|
|
6491
6815
|
report?.({ phase: "validate", done: 1, total: 1 });
|
|
6492
|
-
const
|
|
6816
|
+
const noteClusters = manifest.layout?.clusters?.notes;
|
|
6817
|
+
const parsedNotes = noteClusters && noteClusters.length > 0 ? [...noteClusters].sort((a, b) => a.offset - b.offset).flatMap((ref) => parseJsonl(files.get(ref.href))) : parseJsonl(files.get("notes.jsonl"));
|
|
6493
6818
|
const parsedCollections = parseJsonArray(files.get("collections.json"));
|
|
6494
6819
|
parseJsonArray(files.get("tags.json"));
|
|
6495
6820
|
const parsedLinks = parseJsonl(files.get("links.jsonl"));
|
|
@@ -6915,6 +7240,354 @@ function parseJsonArray(data) {
|
|
|
6915
7240
|
return JSON.parse(decoder.decode(data));
|
|
6916
7241
|
}
|
|
6917
7242
|
|
|
7243
|
+
// src/shard/shard-reader.ts
|
|
7244
|
+
var decoder2 = new TextDecoder();
|
|
7245
|
+
function parseJsonlBytes(data) {
|
|
7246
|
+
if (!data || data.byteLength === 0) return [];
|
|
7247
|
+
return decoder2.decode(data).split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
7248
|
+
}
|
|
7249
|
+
function parseJsonArrayBytes(data) {
|
|
7250
|
+
if (!data || data.byteLength === 0) return [];
|
|
7251
|
+
return JSON.parse(decoder2.decode(data));
|
|
7252
|
+
}
|
|
7253
|
+
var DEFAULT_WEIGHTS = { title: 4, content: 1, tag: 2 };
|
|
7254
|
+
async function toBytes(blobOrBytes) {
|
|
7255
|
+
if (blobOrBytes instanceof Uint8Array) return blobOrBytes;
|
|
7256
|
+
return new Uint8Array(await blobOrBytes.arrayBuffer());
|
|
7257
|
+
}
|
|
7258
|
+
var PackedComponentStore = class {
|
|
7259
|
+
manifest;
|
|
7260
|
+
files;
|
|
7261
|
+
constructor(files, manifest) {
|
|
7262
|
+
this.files = files;
|
|
7263
|
+
this.manifest = manifest;
|
|
7264
|
+
}
|
|
7265
|
+
read(filename) {
|
|
7266
|
+
return Promise.resolve(this.files.get(filename));
|
|
7267
|
+
}
|
|
7268
|
+
};
|
|
7269
|
+
var UrlComponentStore = class {
|
|
7270
|
+
manifest;
|
|
7271
|
+
baseUrl;
|
|
7272
|
+
fetchImpl;
|
|
7273
|
+
cache = /* @__PURE__ */ new Map();
|
|
7274
|
+
constructor(baseUrl, fetchImpl, manifest) {
|
|
7275
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
7276
|
+
this.fetchImpl = fetchImpl;
|
|
7277
|
+
this.manifest = manifest;
|
|
7278
|
+
}
|
|
7279
|
+
async read(filename) {
|
|
7280
|
+
if (this.cache.has(filename)) return this.cache.get(filename);
|
|
7281
|
+
const response = await this.fetchImpl(`${this.baseUrl}/${filename}`);
|
|
7282
|
+
if (!response.ok) {
|
|
7283
|
+
this.cache.set(filename, void 0);
|
|
7284
|
+
return void 0;
|
|
7285
|
+
}
|
|
7286
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
7287
|
+
this.cache.set(filename, bytes);
|
|
7288
|
+
return bytes;
|
|
7289
|
+
}
|
|
7290
|
+
};
|
|
7291
|
+
async function resolveStore(source) {
|
|
7292
|
+
if (typeof source === "object" && "baseUrl" in source) {
|
|
7293
|
+
const fetchImpl = source.fetchImpl ?? globalThis.fetch;
|
|
7294
|
+
const base = source.baseUrl.replace(/\/$/, "");
|
|
7295
|
+
const manifestResponse = await fetchImpl(`${base}/manifest.json`);
|
|
7296
|
+
if (!manifestResponse.ok) {
|
|
7297
|
+
throw new Error(`Failed to fetch shard manifest (${manifestResponse.status}): ${base}/manifest.json`);
|
|
7298
|
+
}
|
|
7299
|
+
const manifest2 = await manifestResponse.json();
|
|
7300
|
+
return new UrlComponentStore(base, fetchImpl, manifest2);
|
|
7301
|
+
}
|
|
7302
|
+
const bytes = await toBytes(source);
|
|
7303
|
+
const files = unpackTarGz(bytes);
|
|
7304
|
+
const manifestBytes = files.get("manifest.json");
|
|
7305
|
+
if (!manifestBytes) throw new Error("Missing manifest.json in shard archive");
|
|
7306
|
+
const manifest = JSON.parse(decoder2.decode(manifestBytes));
|
|
7307
|
+
return new PackedComponentStore(files, manifest);
|
|
7308
|
+
}
|
|
7309
|
+
function tokenize(query) {
|
|
7310
|
+
return query.toLowerCase().split(/[^a-z0-9]+/i).filter((token) => token.length > 0);
|
|
7311
|
+
}
|
|
7312
|
+
function noteSearchText(note) {
|
|
7313
|
+
return `${note.title ?? ""} ${note.original_content} ${note.revised_content ?? ""}`.toLowerCase();
|
|
7314
|
+
}
|
|
7315
|
+
function countOccurrences(haystack, needle) {
|
|
7316
|
+
if (!needle) return 0;
|
|
7317
|
+
let count = 0;
|
|
7318
|
+
let index = haystack.indexOf(needle);
|
|
7319
|
+
while (index !== -1) {
|
|
7320
|
+
count += 1;
|
|
7321
|
+
index = haystack.indexOf(needle, index + needle.length);
|
|
7322
|
+
}
|
|
7323
|
+
return count;
|
|
7324
|
+
}
|
|
7325
|
+
function noteMatchesTokens(note, tokens) {
|
|
7326
|
+
if (tokens.length === 0) return true;
|
|
7327
|
+
const text = noteSearchText(note);
|
|
7328
|
+
const tagText = note.tags.join(" ").toLowerCase();
|
|
7329
|
+
return tokens.every((token) => text.includes(token) || tagText.includes(token));
|
|
7330
|
+
}
|
|
7331
|
+
function rankNote(note, tokens, weights) {
|
|
7332
|
+
if (tokens.length === 0) return 0;
|
|
7333
|
+
const title = (note.title ?? "").toLowerCase();
|
|
7334
|
+
const content = `${note.original_content} ${note.revised_content ?? ""}`.toLowerCase();
|
|
7335
|
+
const tagText = note.tags.join(" ").toLowerCase();
|
|
7336
|
+
let score = 0;
|
|
7337
|
+
for (const token of tokens) {
|
|
7338
|
+
score += countOccurrences(title, token) * weights.title;
|
|
7339
|
+
score += countOccurrences(content, token) * weights.content;
|
|
7340
|
+
if (tagText.includes(token)) score += weights.tag;
|
|
7341
|
+
}
|
|
7342
|
+
return score;
|
|
7343
|
+
}
|
|
7344
|
+
function makeSnippet(note, tokens, length) {
|
|
7345
|
+
const content = `${note.original_content} ${note.revised_content ?? ""}`.trim();
|
|
7346
|
+
if (tokens.length === 0) return content.slice(0, length);
|
|
7347
|
+
const lower = content.toLowerCase();
|
|
7348
|
+
const firstAt = tokens.map((token) => lower.indexOf(token)).filter((index) => index !== -1).sort((a, b) => a - b)[0] ?? 0;
|
|
7349
|
+
const start = Math.max(0, firstAt - Math.floor(length / 3));
|
|
7350
|
+
return (start > 0 ? "\u2026" : "") + content.slice(start, start + length) + (start + length < content.length ? "\u2026" : "");
|
|
7351
|
+
}
|
|
7352
|
+
function passesFilters(note, options) {
|
|
7353
|
+
if (!options.includeDeleted && note.deleted_at) return false;
|
|
7354
|
+
if (options.includeArchived === false && note.archived) return false;
|
|
7355
|
+
if (options.tags && !options.tags.every((tag) => note.tags.includes(tag))) return false;
|
|
7356
|
+
if (options.source && !options.source.includes(note.source)) return false;
|
|
7357
|
+
return true;
|
|
7358
|
+
}
|
|
7359
|
+
function facetCounts(notes) {
|
|
7360
|
+
const tags = {};
|
|
7361
|
+
const source = {};
|
|
7362
|
+
for (const note of notes) {
|
|
7363
|
+
for (const tag of note.tags) tags[tag] = (tags[tag] ?? 0) + 1;
|
|
7364
|
+
source[note.source] = (source[note.source] ?? 0) + 1;
|
|
7365
|
+
}
|
|
7366
|
+
return { tags, source };
|
|
7367
|
+
}
|
|
7368
|
+
var ShardReaderImpl = class {
|
|
7369
|
+
manifest;
|
|
7370
|
+
store;
|
|
7371
|
+
options;
|
|
7372
|
+
noteClusterCache = /* @__PURE__ */ new Map();
|
|
7373
|
+
allNotes = null;
|
|
7374
|
+
links = null;
|
|
7375
|
+
noteSkos = null;
|
|
7376
|
+
concepts = null;
|
|
7377
|
+
matchCache = /* @__PURE__ */ new Map();
|
|
7378
|
+
maxCachedMatches;
|
|
7379
|
+
semanticPrepared = false;
|
|
7380
|
+
constructor(store, options) {
|
|
7381
|
+
this.store = store;
|
|
7382
|
+
this.manifest = store.manifest;
|
|
7383
|
+
this.options = options;
|
|
7384
|
+
this.maxCachedMatches = options.maxCachedMatches && options.maxCachedMatches > 0 ? options.maxCachedMatches : 5e3;
|
|
7385
|
+
}
|
|
7386
|
+
noteClusters() {
|
|
7387
|
+
return this.manifest.layout?.clusters?.notes ?? null;
|
|
7388
|
+
}
|
|
7389
|
+
async readCluster(ref) {
|
|
7390
|
+
const cached = this.noteClusterCache.get(ref.href);
|
|
7391
|
+
if (cached) return { notes: cached, fetched: false };
|
|
7392
|
+
const notes = parseJsonlBytes(await this.store.read(ref.href));
|
|
7393
|
+
this.noteClusterCache.set(ref.href, notes);
|
|
7394
|
+
return { notes, fetched: true };
|
|
7395
|
+
}
|
|
7396
|
+
/** Load every note (clustered or monolithic). Returns count of cluster files fetched. */
|
|
7397
|
+
async loadAllNotes() {
|
|
7398
|
+
const clusters = this.noteClusters();
|
|
7399
|
+
if (!clusters) {
|
|
7400
|
+
if (!this.allNotes) {
|
|
7401
|
+
this.allNotes = parseJsonlBytes(await this.store.read("notes.jsonl"));
|
|
7402
|
+
return { notes: this.allNotes, fetched: 1 };
|
|
7403
|
+
}
|
|
7404
|
+
return { notes: this.allNotes, fetched: 0 };
|
|
7405
|
+
}
|
|
7406
|
+
const all = [];
|
|
7407
|
+
let fetched = 0;
|
|
7408
|
+
for (const ref of clusters) {
|
|
7409
|
+
const loaded = await this.readCluster(ref);
|
|
7410
|
+
if (loaded.fetched) fetched += 1;
|
|
7411
|
+
all.push(...loaded.notes);
|
|
7412
|
+
}
|
|
7413
|
+
return { notes: all, fetched };
|
|
7414
|
+
}
|
|
7415
|
+
async listNotes(options = {}) {
|
|
7416
|
+
const offset = options.offset ?? 0;
|
|
7417
|
+
const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
|
|
7418
|
+
const { notes } = await this.loadAllNotes();
|
|
7419
|
+
const filtered = notes.filter((note) => passesFilters(note, options));
|
|
7420
|
+
const page = filtered.slice(offset, offset + limit);
|
|
7421
|
+
return { items: page.map(noteFromShard), total: filtered.length };
|
|
7422
|
+
}
|
|
7423
|
+
async getNote(id) {
|
|
7424
|
+
const { notes } = await this.loadAllNotes();
|
|
7425
|
+
const found = notes.find((note) => note.id === id);
|
|
7426
|
+
return found ? noteFromShard(found) : null;
|
|
7427
|
+
}
|
|
7428
|
+
matchKey(query, options) {
|
|
7429
|
+
return JSON.stringify({
|
|
7430
|
+
q: query.trim().toLowerCase(),
|
|
7431
|
+
tags: options.tags ?? null,
|
|
7432
|
+
source: options.source ?? null,
|
|
7433
|
+
includeArchived: options.includeArchived ?? true,
|
|
7434
|
+
includeDeleted: options.includeDeleted ?? false,
|
|
7435
|
+
weights: { ...DEFAULT_WEIGHTS, ...options.weights }
|
|
7436
|
+
});
|
|
7437
|
+
}
|
|
7438
|
+
cacheMatches(key, notes) {
|
|
7439
|
+
this.matchCache.delete(key);
|
|
7440
|
+
this.matchCache.set(key, notes);
|
|
7441
|
+
let total = 0;
|
|
7442
|
+
for (const set of this.matchCache.values()) total += set.length;
|
|
7443
|
+
while (total > this.maxCachedMatches && this.matchCache.size > 1) {
|
|
7444
|
+
const oldest = this.matchCache.keys().next().value;
|
|
7445
|
+
if (oldest === void 0 || oldest === key) break;
|
|
7446
|
+
total -= this.matchCache.get(oldest)?.length ?? 0;
|
|
7447
|
+
this.matchCache.delete(oldest);
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
7450
|
+
async search(query, options = {}) {
|
|
7451
|
+
const tokens = tokenize(query);
|
|
7452
|
+
const weights = { ...DEFAULT_WEIGHTS, ...options.weights };
|
|
7453
|
+
const offset = options.offset ?? 0;
|
|
7454
|
+
const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
|
|
7455
|
+
const key = this.matchKey(query, options);
|
|
7456
|
+
let matched = this.matchCache.get(key);
|
|
7457
|
+
let fetchedClusters = 0;
|
|
7458
|
+
if (matched) {
|
|
7459
|
+
this.matchCache.delete(key);
|
|
7460
|
+
this.matchCache.set(key, matched);
|
|
7461
|
+
} else {
|
|
7462
|
+
const { notes, fetched } = await this.loadAllNotes();
|
|
7463
|
+
fetchedClusters = fetched;
|
|
7464
|
+
matched = notes.filter((note) => passesFilters(note, options) && noteMatchesTokens(note, tokens));
|
|
7465
|
+
if (tokens.length > 0 && options.rank !== false) {
|
|
7466
|
+
matched = [...matched].sort((a, b) => rankNote(b, tokens, weights) - rankNote(a, tokens, weights));
|
|
7467
|
+
}
|
|
7468
|
+
this.cacheMatches(key, matched);
|
|
7469
|
+
}
|
|
7470
|
+
const page = matched.slice(offset, offset + limit);
|
|
7471
|
+
const result = {
|
|
7472
|
+
items: page.map(noteFromShard),
|
|
7473
|
+
total: matched.length,
|
|
7474
|
+
facets: facetCounts(matched),
|
|
7475
|
+
fetchedClusters
|
|
7476
|
+
};
|
|
7477
|
+
if (options.rank || options.snippets) {
|
|
7478
|
+
const snippetLength = options.snippetLength ?? 160;
|
|
7479
|
+
result.rankedItems = page.map((note) => ({
|
|
7480
|
+
note: noteFromShard(note),
|
|
7481
|
+
rank: rankNote(note, tokens, weights),
|
|
7482
|
+
...options.snippets ? { snippet: makeSnippet(note, tokens, snippetLength) } : {}
|
|
7483
|
+
}));
|
|
7484
|
+
}
|
|
7485
|
+
return result;
|
|
7486
|
+
}
|
|
7487
|
+
async loadLinks() {
|
|
7488
|
+
if (!this.links) this.links = parseJsonlBytes(await this.store.read("links.jsonl"));
|
|
7489
|
+
return this.links;
|
|
7490
|
+
}
|
|
7491
|
+
async linksOf(id) {
|
|
7492
|
+
const links = await this.loadLinks();
|
|
7493
|
+
return links.filter((link) => link.from_note_id === id || link.to_note_id === id);
|
|
7494
|
+
}
|
|
7495
|
+
async loadConcepts() {
|
|
7496
|
+
if (!this.noteSkos) {
|
|
7497
|
+
this.noteSkos = parseJsonlBytes(await this.store.read("note_skos_tags.jsonl"));
|
|
7498
|
+
}
|
|
7499
|
+
if (!this.concepts) {
|
|
7500
|
+
const list = parseJsonArrayBytes(await this.store.read("skos_concepts.json"));
|
|
7501
|
+
this.concepts = new Map(list.map((concept) => [concept.id, concept]));
|
|
7502
|
+
}
|
|
7503
|
+
return { noteSkos: this.noteSkos, concepts: this.concepts };
|
|
7504
|
+
}
|
|
7505
|
+
async conceptsOf(id) {
|
|
7506
|
+
const { noteSkos, concepts } = await this.loadConcepts();
|
|
7507
|
+
const out = [];
|
|
7508
|
+
for (const tag of noteSkos) {
|
|
7509
|
+
if (tag.note_id !== id) continue;
|
|
7510
|
+
const concept = concepts.get(tag.concept_id);
|
|
7511
|
+
if (concept) out.push(concept);
|
|
7512
|
+
}
|
|
7513
|
+
return out;
|
|
7514
|
+
}
|
|
7515
|
+
async getNoteFull(id) {
|
|
7516
|
+
const note = await this.getNote(id);
|
|
7517
|
+
if (!note) return null;
|
|
7518
|
+
const [links, concepts] = await Promise.all([this.linksOf(id), this.conceptsOf(id)]);
|
|
7519
|
+
return { note, links, concepts };
|
|
7520
|
+
}
|
|
7521
|
+
async semantic(query, k = 10) {
|
|
7522
|
+
const provider = this.options.semantic;
|
|
7523
|
+
if (!provider) return [];
|
|
7524
|
+
if (!this.semanticPrepared) {
|
|
7525
|
+
await provider.prepare?.(this.store);
|
|
7526
|
+
this.semanticPrepared = true;
|
|
7527
|
+
}
|
|
7528
|
+
const hits = await provider.search(query, k);
|
|
7529
|
+
const { notes } = await this.loadAllNotes();
|
|
7530
|
+
const byId = new Map(notes.map((note) => [note.id, note]));
|
|
7531
|
+
const out = [];
|
|
7532
|
+
for (const hit of hits) {
|
|
7533
|
+
const note = byId.get(hit.id);
|
|
7534
|
+
if (note) out.push({ note: noteFromShard(note), score: hit.score });
|
|
7535
|
+
}
|
|
7536
|
+
return out;
|
|
7537
|
+
}
|
|
7538
|
+
close() {
|
|
7539
|
+
this.noteClusterCache.clear();
|
|
7540
|
+
this.matchCache.clear();
|
|
7541
|
+
this.allNotes = null;
|
|
7542
|
+
this.links = null;
|
|
7543
|
+
this.noteSkos = null;
|
|
7544
|
+
this.concepts = null;
|
|
7545
|
+
}
|
|
7546
|
+
};
|
|
7547
|
+
async function openShard(source, options = {}) {
|
|
7548
|
+
const store = await resolveStore(source);
|
|
7549
|
+
if (store.manifest.min_reader_version && store.manifest.min_reader_version > CURRENT_SHARD_VERSION) {
|
|
7550
|
+
throw new Error(
|
|
7551
|
+
`Shard requires reader version ${store.manifest.min_reader_version}, but this build supports ${CURRENT_SHARD_VERSION}. Import the shard instead.`
|
|
7552
|
+
);
|
|
7553
|
+
}
|
|
7554
|
+
return new ShardReaderImpl(store, options);
|
|
7555
|
+
}
|
|
7556
|
+
|
|
7557
|
+
// src/shard/semantic-providers.ts
|
|
7558
|
+
var decoder3 = new TextDecoder();
|
|
7559
|
+
function cosine(a, b) {
|
|
7560
|
+
let dot = 0;
|
|
7561
|
+
let normA = 0;
|
|
7562
|
+
let normB = 0;
|
|
7563
|
+
const len = Math.min(a.length, b.length);
|
|
7564
|
+
for (let i = 0; i < len; i++) {
|
|
7565
|
+
dot += a[i] * b[i];
|
|
7566
|
+
normA += a[i] * a[i];
|
|
7567
|
+
normB += b[i] * b[i];
|
|
7568
|
+
}
|
|
7569
|
+
if (normA === 0 || normB === 0) return 0;
|
|
7570
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
7571
|
+
}
|
|
7572
|
+
function createCosineSemanticProvider(options) {
|
|
7573
|
+
let vectors = options.vectors ?? [];
|
|
7574
|
+
return {
|
|
7575
|
+
async prepare(store) {
|
|
7576
|
+
if (options.vectors) return;
|
|
7577
|
+
const bytes = await store.read(options.vectorsFile ?? "vectors.jsonl");
|
|
7578
|
+
if (!bytes || bytes.byteLength === 0) {
|
|
7579
|
+
vectors = [];
|
|
7580
|
+
return;
|
|
7581
|
+
}
|
|
7582
|
+
vectors = decoder3.decode(bytes).split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
7583
|
+
},
|
|
7584
|
+
async search(query, k) {
|
|
7585
|
+
const queryVector = await options.embedQuery(query);
|
|
7586
|
+
return vectors.map((entry) => ({ id: entry.id, score: cosine(queryVector, entry.vector) })).sort((a, b) => b.score - a.score).slice(0, k);
|
|
7587
|
+
}
|
|
7588
|
+
};
|
|
7589
|
+
}
|
|
7590
|
+
|
|
6918
7591
|
// src/shard/prefetch.ts
|
|
6919
7592
|
var DEFAULT_CACHE_NAME = "fortemi-shards";
|
|
6920
7593
|
var warmStore = /* @__PURE__ */ new Map();
|
|
@@ -7166,6 +7839,9 @@ function validateAiwgFortemiChunkManifest(value) {
|
|
|
7166
7839
|
if (data.detail !== void 0) {
|
|
7167
7840
|
if (!hasString(data.detail.href)) errors.push("detail.href is required");
|
|
7168
7841
|
else if (!data.detail.href.includes("{id}")) errors.push("detail.href must contain the {id} placeholder");
|
|
7842
|
+
if (data.detail.encoding !== void 0 && data.detail.encoding !== "uri" && data.detail.encoding !== "base64url") {
|
|
7843
|
+
errors.push("detail.encoding must be 'uri' or 'base64url'");
|
|
7844
|
+
}
|
|
7169
7845
|
}
|
|
7170
7846
|
if (!Array.isArray(data?.parts)) errors.push("parts must be an array");
|
|
7171
7847
|
let expectedOffset = 0;
|
|
@@ -7267,10 +7943,20 @@ function createAiwgFetchChunkLoader(baseUrl) {
|
|
|
7267
7943
|
return response.json();
|
|
7268
7944
|
};
|
|
7269
7945
|
}
|
|
7946
|
+
function encodeAiwgDetailId(id, encoding = "base64url") {
|
|
7947
|
+
if (encoding === "uri") return encodeURIComponent(id);
|
|
7948
|
+
const bytes = new TextEncoder().encode(id);
|
|
7949
|
+
let binary = "";
|
|
7950
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
7951
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
7952
|
+
}
|
|
7953
|
+
function aiwgDetailHrefForId(detail, id) {
|
|
7954
|
+
return detail.href.replace("{id}", encodeAiwgDetailId(id, detail.encoding ?? "uri"));
|
|
7955
|
+
}
|
|
7270
7956
|
function createAiwgFetchDetailLoader(baseUrl) {
|
|
7271
7957
|
return async (id, manifest) => {
|
|
7272
7958
|
if (!manifest.detail?.href) throw new Error("Manifest has no detail.href for record resolution");
|
|
7273
|
-
const relative = manifest.detail
|
|
7959
|
+
const relative = aiwgDetailHrefForId(manifest.detail, id);
|
|
7274
7960
|
const href = baseUrl ? new URL(relative, baseUrl).toString() : relative;
|
|
7275
7961
|
const response = await fetch(href);
|
|
7276
7962
|
if (!response.ok) throw new Error("Failed to fetch AIWG index detail " + href + ": " + response.status);
|
|
@@ -7293,6 +7979,8 @@ function getAiwgFortemiFacets(items) {
|
|
|
7293
7979
|
function buildAiwgChunkedIndex(index, options = {}) {
|
|
7294
7980
|
const partSize = hasPositiveInteger(options.partSize) ? options.partSize : 500;
|
|
7295
7981
|
const projection = options.projection;
|
|
7982
|
+
const idEncoding = options.idEncoding ?? "base64url";
|
|
7983
|
+
const detailHref = options.detailHref ?? "detail/{id}.json";
|
|
7296
7984
|
const items = index.items;
|
|
7297
7985
|
const pad = (value) => String(value).padStart(4, "0");
|
|
7298
7986
|
const project = (record) => {
|
|
@@ -7325,12 +8013,16 @@ function buildAiwgChunkedIndex(index, options = {}) {
|
|
|
7325
8013
|
part_size: partSize,
|
|
7326
8014
|
facets: getAiwgFortemiFacets(items),
|
|
7327
8015
|
parts: partRefs,
|
|
7328
|
-
...projection ? { projection, detail: { href:
|
|
8016
|
+
...projection ? { projection, detail: { href: detailHref, encoding: idEncoding } } : {}
|
|
7329
8017
|
};
|
|
7330
8018
|
return {
|
|
7331
8019
|
manifest,
|
|
7332
8020
|
parts,
|
|
7333
|
-
details: projection ? items.map((record) => ({
|
|
8021
|
+
details: projection ? items.map((record) => ({
|
|
8022
|
+
id: record.id,
|
|
8023
|
+
href: aiwgDetailHrefForId({ href: detailHref, encoding: idEncoding }, record.id),
|
|
8024
|
+
record
|
|
8025
|
+
})) : []
|
|
7334
8026
|
};
|
|
7335
8027
|
}
|
|
7336
8028
|
function includesAll(actual, expected) {
|
|
@@ -7440,6 +8132,34 @@ function clampMaxCachedDetails(value) {
|
|
|
7440
8132
|
if (!hasPositiveInteger(value)) return 32;
|
|
7441
8133
|
return value;
|
|
7442
8134
|
}
|
|
8135
|
+
function clampMaxCachedMatches(value) {
|
|
8136
|
+
if (!hasPositiveInteger(value)) return 5e3;
|
|
8137
|
+
return value;
|
|
8138
|
+
}
|
|
8139
|
+
function matchSetCacheKey(q, options) {
|
|
8140
|
+
return JSON.stringify({
|
|
8141
|
+
q,
|
|
8142
|
+
types: options.types ?? null,
|
|
8143
|
+
facets: options.facets ?? null,
|
|
8144
|
+
tags: options.tags ?? null,
|
|
8145
|
+
concepts: options.concepts ?? null,
|
|
8146
|
+
privacy: options.privacy ?? null,
|
|
8147
|
+
rel: options.relationshipTargetId ?? null,
|
|
8148
|
+
weights: { ...DEFAULT_QUERY_WEIGHTS, ...options.weights }
|
|
8149
|
+
});
|
|
8150
|
+
}
|
|
8151
|
+
function cacheMatchEntries(runtime, key, entries) {
|
|
8152
|
+
runtime.matchCache.delete(key);
|
|
8153
|
+
runtime.matchCache.set(key, entries);
|
|
8154
|
+
let total = 0;
|
|
8155
|
+
for (const set of runtime.matchCache.values()) total += set.length;
|
|
8156
|
+
while (total > runtime.maxCachedMatches && runtime.matchCache.size > 1) {
|
|
8157
|
+
const oldest = runtime.matchCache.keys().next().value;
|
|
8158
|
+
if (oldest === void 0 || oldest === key) break;
|
|
8159
|
+
total -= runtime.matchCache.get(oldest)?.length ?? 0;
|
|
8160
|
+
runtime.matchCache.delete(oldest);
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
7443
8163
|
function isDirectChunkBrowse(query, options) {
|
|
7444
8164
|
return query.trim() === "" && !options.rank && !options.snippets && !options.includeMatches && !options.types && !options.facets && !options.tags && !options.concepts && !options.privacy && !options.relationshipTargetId;
|
|
7445
8165
|
}
|
|
@@ -7526,6 +8246,19 @@ async function queryChunkedAiwgFortemiIndex(runtime, query = "", options = {}) {
|
|
|
7526
8246
|
complete: true
|
|
7527
8247
|
};
|
|
7528
8248
|
}
|
|
8249
|
+
const matchKey = matchSetCacheKey(q, options);
|
|
8250
|
+
const cached = runtime.matchCache.get(matchKey);
|
|
8251
|
+
if (cached) {
|
|
8252
|
+
runtime.matchCache.delete(matchKey);
|
|
8253
|
+
runtime.matchCache.set(matchKey, cached);
|
|
8254
|
+
return {
|
|
8255
|
+
...createQueryResultFromRankedEntries(cached, q, options),
|
|
8256
|
+
manifestTotal: runtime.manifest.total,
|
|
8257
|
+
scannedParts: 0,
|
|
8258
|
+
fetchedParts: 0,
|
|
8259
|
+
complete: true
|
|
8260
|
+
};
|
|
8261
|
+
}
|
|
7529
8262
|
const entries = [];
|
|
7530
8263
|
for (const partRef of runtime.manifest.parts) {
|
|
7531
8264
|
const loaded = await loadChunkPart(runtime, partRef);
|
|
@@ -7535,6 +8268,7 @@ async function queryChunkedAiwgFortemiIndex(runtime, query = "", options = {}) {
|
|
|
7535
8268
|
entries.push(...createRankedEntries(loaded.part.items, q, options, partRef.offset));
|
|
7536
8269
|
options.onProgress?.({ phase: "query", done: scannedParts, total: runtime.manifest.parts.length, href: partRef.href });
|
|
7537
8270
|
}
|
|
8271
|
+
cacheMatchEntries(runtime, matchKey, entries);
|
|
7538
8272
|
return {
|
|
7539
8273
|
...createQueryResultFromRankedEntries(entries, q, options),
|
|
7540
8274
|
manifestTotal: runtime.manifest.total,
|
|
@@ -7605,7 +8339,9 @@ function createAiwgIndexController(initialIndex) {
|
|
|
7605
8339
|
partCache: /* @__PURE__ */ new Map(),
|
|
7606
8340
|
detailLoader: options.detailLoader,
|
|
7607
8341
|
maxCachedDetails: clampMaxCachedDetails(options.maxCachedDetails),
|
|
7608
|
-
detailCache: /* @__PURE__ */ new Map()
|
|
8342
|
+
detailCache: /* @__PURE__ */ new Map(),
|
|
8343
|
+
maxCachedMatches: clampMaxCachedMatches(options.maxCachedMatches),
|
|
8344
|
+
matchCache: /* @__PURE__ */ new Map()
|
|
7609
8345
|
};
|
|
7610
8346
|
data = null;
|
|
7611
8347
|
reviewDecisions = [];
|
|
@@ -7665,6 +8401,7 @@ function createAiwgIndexController(initialIndex) {
|
|
|
7665
8401
|
clearChunkCache() {
|
|
7666
8402
|
chunked?.partCache.clear();
|
|
7667
8403
|
chunked?.detailCache.clear();
|
|
8404
|
+
chunked?.matchCache.clear();
|
|
7668
8405
|
error = null;
|
|
7669
8406
|
notify();
|
|
7670
8407
|
},
|
|
@@ -7690,7 +8427,9 @@ function createAiwgIndexController(initialIndex) {
|
|
|
7690
8427
|
notify();
|
|
7691
8428
|
},
|
|
7692
8429
|
createReviewDecisionExport(generatedAt) {
|
|
7693
|
-
|
|
8430
|
+
const source = index ?? (chunked ? { schema_version: "aiwg.fortemi.index.export.v1" } : null);
|
|
8431
|
+
if (!source) throw new Error("No AIWG index export or chunked manifest loaded");
|
|
8432
|
+
return createAiwgReviewDecisionExport(source, reviewDecisions, generatedAt);
|
|
7694
8433
|
},
|
|
7695
8434
|
subscribe(listener) {
|
|
7696
8435
|
listeners.add(listener);
|
|
@@ -7747,6 +8486,6 @@ function communityIdsFor(item, options) {
|
|
|
7747
8486
|
// src/index.ts
|
|
7748
8487
|
var VERSION = "2026.6.4";
|
|
7749
8488
|
|
|
7750
|
-
export { AIWG_SCAN_REQUIRED_FIELDS, ArchiveManager, AttachmentsRepository, CURRENT_SHARD_VERSION, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, CommunitiesRepository, EMBED_REQUEST_KIND, EMBED_RESPONSE_KIND, EmbeddingSetsRepository, FallbackRouter, FortemiToolManifest, GetNoteInputSchema, GraphRepository, JOB_CAPABILITIES, JOB_PRIORITIES, JobQueueWorker, LOCAL_ENDPOINTS, LinksRepository, ListNotesInputSchema, ManageArchiveInputSchema, ManageAttachmentsInputSchema, ManageCapabilitiesInputSchema, ManageCollectionsInputSchema, ManageLinksInputSchema, ManageNoteInputSchema, ManageTagsInputSchema, MemoryBlobStore, MigrationRunner, NotesRepository, OpenAICompatibleProvider, PGliteStorageBackend, PGliteStorageBackendFactory, PGliteWorkerClient, PGliteWorkerStorageBackend, PGliteWorkerStorageBackendFactory, ProviderRegistry, SHARD_FORMAT, SearchInputSchema, SearchRepository, SkosRepository, TagsRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, aiwgFortemiIndexToCommunityGraph, allMigrations, appendPluginScript, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, buildAiwgChunkedIndex, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, clearPrefetchedShard, collectionFromShard, collectionToShard, computeHash, computeSri, conceptTaggingHandler, cosineSimilarity, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, createBlobStore, createCspReportHandler, createFortemi, createLegacyProvider, createPGliteInstance, createRoutes, createWorkerEmbedFunction, defaultStorageBackendFactory, detectCommunities, detectGpuCapabilities, detectInferenceCapabilities, discoverLocalProviders, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, exportShard, fetchPluginScript, fortemiManifest, fromPrefetched, generateId, getAiwgFortemiFacets, getEmbedFunction, getFortemiBridge, getFortemiSecretStore, getJobQueueStatus, getLlmFunction, getNote, getPrefetchedSha256, handleEmbedRequests, hasFortemiSecureSecrets, importShard, isPluginScriptAllowed, isShardPrefetched, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, noteFromShard, noteSkosTagToShard, noteToShard, packTarGz, parseCspReport, prefetchShard, provenanceEdgeToShard, queryAiwgFortemiIndex, registerLlmCapability, registerSemanticCapability, registerSemanticCapabilityWorker, registerServiceWorker, searchTool, selectLlmModel, setEmbedFunction, setLlmFunction, sha256Hex, skosConceptToShard, skosRelationToShard, skosSchemeToShard, suggestTags, tagsFromShard, tagsToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateChecksums, verifySri };
|
|
8489
|
+
export { AIWG_SCAN_REQUIRED_FIELDS, ArchiveManager, AttachmentsRepository, CURRENT_MIGRATION_HEAD, CURRENT_SHARD_VERSION, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, CommunitiesRepository, DB_SNAPSHOT_SCHEMA_VERSION, DbSnapshotVersionError, EMBED_REQUEST_KIND, EMBED_RESPONSE_KIND, EmbeddingSetsRepository, FallbackRouter, FortemiToolManifest, GetNoteInputSchema, GraphRepository, JOB_CAPABILITIES, JOB_PRIORITIES, JobQueueWorker, LOCAL_ENDPOINTS, LinksRepository, ListNotesInputSchema, ManageArchiveInputSchema, ManageAttachmentsInputSchema, ManageCapabilitiesInputSchema, ManageCollectionsInputSchema, ManageLinksInputSchema, ManageNoteInputSchema, ManageTagsInputSchema, MemoryBlobStore, MigrationRunner, NotesRepository, OpenAICompatibleProvider, PGliteStorageBackend, PGliteStorageBackendFactory, PGliteWorkerClient, PGliteWorkerStorageBackend, PGliteWorkerStorageBackendFactory, ProviderRegistry, SHARD_FORMAT, SUPPORTED_PGLITE_VERSION, SearchInputSchema, SearchRepository, SkosRepository, TagsRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, aiwgFortemiIndexToCommunityGraph, allMigrations, appendPluginScript, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, buildAiwgChunkedIndex, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, clearPrefetchedShard, collectionFromShard, collectionToShard, computeHash, computeSri, conceptTaggingHandler, cosineSimilarity, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, createBlobStore, createCosineSemanticProvider, createCspReportHandler, createFortemi, createLegacyProvider, createPGliteBackend, createPGliteInstance, createRoutes, createShardBackend, createWorkerEmbedFunction, defaultStorageBackendFactory, detectCommunities, detectGpuCapabilities, detectInferenceCapabilities, discoverLocalProviders, dumpDbSnapshot, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, exportShard, fetchPluginScript, fortemiManifest, fromPrefetched, generateId, getAiwgFortemiFacets, getEmbedFunction, getFortemiBridge, getFortemiSecretStore, getJobQueueStatus, getLlmFunction, getNote, getPrefetchedSha256, handleEmbedRequests, hasFortemiSecureSecrets, importShard, isPluginScriptAllowed, isShardPrefetched, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, noteFromShard, noteSkosTagToShard, noteToShard, openShard, packTarGz, parseCspReport, prefetchShard, provenanceEdgeToShard, queryAiwgFortemiIndex, registerLlmCapability, registerSemanticCapability, registerSemanticCapabilityWorker, registerServiceWorker, restoreDbSnapshot, searchTool, selectBackend, selectLlmModel, setEmbedFunction, setLlmFunction, sha256Hex, skosConceptToShard, skosRelationToShard, skosSchemeToShard, suggestTags, tagsFromShard, tagsToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateChecksums, verifyDbSnapshotMeta, verifySri };
|
|
7751
8490
|
//# sourceMappingURL=index.js.map
|
|
7752
8491
|
//# sourceMappingURL=index.js.map
|