@equationalapplications/core-llm-wiki 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/{chunk-J3N3WRK7.mjs → chunk-KIJINNYU.mjs} +367 -84
- package/dist/chunk-KIJINNYU.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +664 -103
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +300 -24
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-DyXISRwS.d.mts → testing-zf3Jp_RL.d.mts} +336 -29
- package/dist/{testing-DyXISRwS.d.ts → testing-zf3Jp_RL.d.ts} +336 -29
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +378 -74
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-J3N3WRK7.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -825,6 +825,20 @@ const { chunks, truncated } = chunkText(
|
|
|
825
825
|
|
|
826
826
|
> **Note:** `ingestDocument()` clamps the resolved overlap — whether it came from `chunkOverlap`, `WikiConfig`, or `DEFAULT_CHUNK_OVERLAP` — to `maxChunkLength - 1`. The clamp is evaluated on every call and is a no-op at the shipped defaults (`400 < 12000 - 1`), but it also bites when a custom `maxChunkLength` alone leaves the resolved overlap too large (e.g. `maxChunkLength: 100` with the default overlap `400` ingests at an effective overlap of `99`). That clamp is internal: passing the unclamped pair straight to `chunkText` throws, since it requires `overlap < maxChunkLength`. Apply the same `Math.min(overlap, maxChunkLength - 1)` yourself when re-chunking under a custom config.
|
|
827
827
|
|
|
828
|
+
## Schema Migrations
|
|
829
|
+
|
|
830
|
+
`wikiMemory.setup()` runs pending schema migrations in order and records the applied version. Migrations are additive and safe to re-run.
|
|
831
|
+
|
|
832
|
+
**Migration v9** (`add_live_hash_unique_index`) adds a partial `UNIQUE` index on `(entity_id, source_hash)` scoped to live (`deleted_at IS NULL`), non-null-hash rows. This closes a TOCTOU race where two concurrent `ingestDocument()` calls for different `sourceRef`s could both pass the duplicate-hash pre-check before either write committed, leaving two live rows with the same content hash.
|
|
833
|
+
|
|
834
|
+
If your existing database already has live rows that violate this invariant (created before the app-level race fix shipped), `setup()` throws:
|
|
835
|
+
|
|
836
|
+
```text
|
|
837
|
+
Migration v9 (add_live_hash_unique_index) failed: existing live rows violate the new UNIQUE index. …
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
The error lists the offending `(entity_id, source_hash)` groups. `setup()` performs no destructive cleanup on this path — the index is not created and the schema version is not advanced, so it's safe to retry after remediation. To remediate, either soft-delete (or re-ingest with a corrected `sourceRef`) all but one live row per listed group, then re-run `setup()`.
|
|
841
|
+
|
|
828
842
|
## Adapter Interface
|
|
829
843
|
|
|
830
844
|
Implement `SQLiteAdapter` to use your platform's SQLite driver:
|
|
@@ -38,6 +38,12 @@ var SQLITE_RESULT_CODE_NAMES = {
|
|
|
38
38
|
26: "SQLITE_NOTADB",
|
|
39
39
|
27: "SQLITE_NOTICE",
|
|
40
40
|
28: "SQLITE_WARNING",
|
|
41
|
+
// Extended result codes (https://sqlite.org/rescode.html#extended_result_code_list).
|
|
42
|
+
// 2067 = SQLITE_CONSTRAINT_UNIQUE — raised when a UNIQUE index/PRIMARY KEY
|
|
43
|
+
// rejects an INSERT/UPDATE; expo-sqlite reports this as a numeric "Error code N:"
|
|
44
|
+
// prefix and we normalize it back to the symbolic name so callers can branch on
|
|
45
|
+
// the same string the rest of the ecosystem uses.
|
|
46
|
+
2067: "SQLITE_CONSTRAINT_UNIQUE",
|
|
41
47
|
100: "SQLITE_ROW",
|
|
42
48
|
101: "SQLITE_DONE"
|
|
43
49
|
};
|
|
@@ -73,6 +79,31 @@ var WikiDuplicateHashError = class extends Error {
|
|
|
73
79
|
this.entityId = params.entityId;
|
|
74
80
|
}
|
|
75
81
|
};
|
|
82
|
+
var WikiStrictOntologyViolation = class extends Error {
|
|
83
|
+
constructor(entityId, kind, type) {
|
|
84
|
+
super(
|
|
85
|
+
`Out-of-manifest ${kind} type "${type}" for entity "${entityId}" under strict mode.`
|
|
86
|
+
);
|
|
87
|
+
this.code = "WIKI_STRICT_ONTOLOGY_VIOLATION";
|
|
88
|
+
this.entityId = entityId;
|
|
89
|
+
this.kind = kind;
|
|
90
|
+
this.type = type;
|
|
91
|
+
this.name = "WikiStrictOntologyViolation";
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var WikiSourceRefHashCollision = class extends Error {
|
|
95
|
+
constructor(params) {
|
|
96
|
+
super(
|
|
97
|
+
`Source hash "${params.sourceHash}" for entity "${params.entityId}" is already mapped to sourceRef "${params.existingSourceRef}"; cannot remap to "${params.attemptedSourceRef}".`
|
|
98
|
+
);
|
|
99
|
+
this.code = "WIKI_SOURCE_REF_HASH_COLLISION";
|
|
100
|
+
this.entityId = params.entityId;
|
|
101
|
+
this.sourceHash = params.sourceHash;
|
|
102
|
+
this.existingSourceRef = params.existingSourceRef;
|
|
103
|
+
this.attemptedSourceRef = params.attemptedSourceRef;
|
|
104
|
+
this.name = "WikiSourceRefHashCollision";
|
|
105
|
+
}
|
|
106
|
+
};
|
|
76
107
|
var WikiTransactionError = class extends Error {
|
|
77
108
|
constructor(message, options) {
|
|
78
109
|
super(message, options);
|
|
@@ -205,14 +236,25 @@ function mergeOntologyUpdates(current, updates) {
|
|
|
205
236
|
}
|
|
206
237
|
return { node_types, edge_types };
|
|
207
238
|
}
|
|
208
|
-
function validateInlineEdges(sourceType, _targetType, edges, manifest) {
|
|
209
|
-
|
|
239
|
+
function validateInlineEdges(sourceType, _targetType, edges, manifest, opts) {
|
|
240
|
+
const strict = opts?.strict === true;
|
|
241
|
+
const entityId = opts?.entityId ?? "";
|
|
242
|
+
if (!Array.isArray(edges)) {
|
|
243
|
+
if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", "");
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
210
246
|
const valid = [];
|
|
211
247
|
for (const edge of edges) {
|
|
212
|
-
if (typeof edge?.edge_type !== "string" || typeof edge?.target_title !== "string")
|
|
248
|
+
if (typeof edge?.edge_type !== "string" || typeof edge?.target_title !== "string") {
|
|
249
|
+
if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", String(edge?.edge_type ?? ""));
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
213
252
|
const defs = resolveEdgeDefinitions(edge.edge_type, manifest);
|
|
214
253
|
const match = defs.find((d) => d.source_type.toLowerCase() === sourceType.toLowerCase());
|
|
215
|
-
if (!match)
|
|
254
|
+
if (!match) {
|
|
255
|
+
if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", edge.edge_type);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
216
258
|
valid.push({ edge_type: match.type, target_title: edge.target_title });
|
|
217
259
|
}
|
|
218
260
|
return valid;
|
|
@@ -500,6 +542,19 @@ var JobManager = class {
|
|
|
500
542
|
this.prefix = prefix;
|
|
501
543
|
this.activeMaintenanceJobs = /* @__PURE__ */ new Set();
|
|
502
544
|
this.activeIngestJobs = /* @__PURE__ */ new Map();
|
|
545
|
+
/**
|
|
546
|
+
* Per-(entityId, sourceHash) promise-chain lock. Serializes ingest races
|
|
547
|
+
* at the application level (the v9 partial UNIQUE index enforces it at
|
|
548
|
+
* the DB level as defense-in-depth). Two callers holding the same hash
|
|
549
|
+
* see FIFO order; different hashes never block one another.
|
|
550
|
+
*
|
|
551
|
+
* Keyed `${entityId}\0${sourceHash}` so distinct (entityId, sourceHash)
|
|
552
|
+
* pairs can never share a lock. A raw concatenation would let
|
|
553
|
+
* ("ab", "c…") collide with ("a", "bc…"); the NUL separator makes that
|
|
554
|
+
* impossible because neither component can contain NUL (entityIds are
|
|
555
|
+
* user-supplied identifiers; sourceHashes are hex digests).
|
|
556
|
+
*/
|
|
557
|
+
this.hashLocks = /* @__PURE__ */ new Map();
|
|
503
558
|
this.statusSubscribers = /* @__PURE__ */ new Map();
|
|
504
559
|
/**
|
|
505
560
|
* Lookup table for acquireLock/releaseLock's dynamic-dispatch branch.
|
|
@@ -517,6 +572,51 @@ var JobManager = class {
|
|
|
517
572
|
ontologyBackfill: (id) => this._ontologyBackfillKey(id)
|
|
518
573
|
};
|
|
519
574
|
}
|
|
575
|
+
_hashLockKey(entityId, sourceHash) {
|
|
576
|
+
return `${entityId}\0${sourceHash}`;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Acquire a promise-chain lock scoped to one `(entityId, sourceHash)` pair.
|
|
580
|
+
* Returns a zero-argument release closure that removes the current tail
|
|
581
|
+
* from the chain. Releasing is idempotent so a holder that double-fires
|
|
582
|
+
* (e.g. on a finally that races an explicit release) cannot unblock
|
|
583
|
+
* the next holder prematurely.
|
|
584
|
+
*
|
|
585
|
+
* Documented ordering: ingest acquires the hash lock FIRST, then the
|
|
586
|
+
* synchronous sourceRef ingest lock, and the release closure unwinds in
|
|
587
|
+
* the opposite order (sourceRef first, hash second). Hash-then-sourceRef
|
|
588
|
+
* is required because the v9 partial UNIQUE index conflicts on the
|
|
589
|
+
* (entity_id, source_hash) pair regardless of source_ref, so two callers
|
|
590
|
+
* with DIFFERENT source_refs racing the same hash are the exact case the
|
|
591
|
+
* TOCTOU race fix needs to serialize.
|
|
592
|
+
*
|
|
593
|
+
* Mechanics: each holder owns a private `released` promise that resolves
|
|
594
|
+
* only when THIS holder calls release(). The map stores the holder's
|
|
595
|
+
* `released` promise as the "tail" — the NEXT caller awaits that tail
|
|
596
|
+
* via `previous = this.hashLocks.get(key)`. The current caller gets
|
|
597
|
+
* their unique release function once `previous` (the prior holder's
|
|
598
|
+
* release signal) settles, so each holder is guaranteed a UNIQUE release
|
|
599
|
+
* closure bound to their own `released` promise.
|
|
600
|
+
*/
|
|
601
|
+
acquireHashLock(entityId, sourceHash) {
|
|
602
|
+
const key = this._hashLockKey(entityId, sourceHash);
|
|
603
|
+
const previous = this.hashLocks.get(key) ?? Promise.resolve();
|
|
604
|
+
let resolveReleased;
|
|
605
|
+
const released = new Promise((resolve) => {
|
|
606
|
+
resolveReleased = resolve;
|
|
607
|
+
});
|
|
608
|
+
let fired = false;
|
|
609
|
+
const release = () => {
|
|
610
|
+
if (fired) return;
|
|
611
|
+
fired = true;
|
|
612
|
+
if (this.hashLocks.get(key) === released) {
|
|
613
|
+
this.hashLocks.delete(key);
|
|
614
|
+
}
|
|
615
|
+
resolveReleased();
|
|
616
|
+
};
|
|
617
|
+
this.hashLocks.set(key, released);
|
|
618
|
+
return previous.then(() => release);
|
|
619
|
+
}
|
|
520
620
|
_pruneKey(entityId) {
|
|
521
621
|
return `${this.prefix}:${entityId}:prune`;
|
|
522
622
|
}
|
|
@@ -730,6 +830,38 @@ var JobManager = class {
|
|
|
730
830
|
this._notifyStatusSubscribers(entityId);
|
|
731
831
|
return true;
|
|
732
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* Centralized ingest lock acquisition. Acquires the hash lock FIRST
|
|
835
|
+
* (FIFO across callers racing the same hash), then the synchronous
|
|
836
|
+
* sourceRef ingest lock (rejects cross-entity conflicts and busy
|
|
837
|
+
* operations). Returns a single zero-argument release closure that
|
|
838
|
+
* unwinds in the opposite order — sourceRef first, hash second — so
|
|
839
|
+
* the sourceRef lock is freed the instant its work is done and only
|
|
840
|
+
* the hash lock keeps serializing the duplicate-content race window.
|
|
841
|
+
*
|
|
842
|
+
* On a sourceRef conflict (WikiBusyError thrown synchronously by the
|
|
843
|
+
* underlying acquireLock) the hash lock is released BEFORE the error
|
|
844
|
+
* propagates, so the next caller in the hash chain is not held up by
|
|
845
|
+
* a request that never reached the DB.
|
|
846
|
+
*/
|
|
847
|
+
async acquireIngestLocks(entityId, sourceRef, sourceHash) {
|
|
848
|
+
const releaseHash = await this.acquireHashLock(entityId, sourceHash);
|
|
849
|
+
let sourceLockAcquired = false;
|
|
850
|
+
try {
|
|
851
|
+
this.acquireLock("ingest", entityId, sourceRef);
|
|
852
|
+
sourceLockAcquired = true;
|
|
853
|
+
} catch (err) {
|
|
854
|
+
releaseHash();
|
|
855
|
+
throw err;
|
|
856
|
+
}
|
|
857
|
+
return () => {
|
|
858
|
+
try {
|
|
859
|
+
if (sourceLockAcquired) this.releaseLock("ingest", entityId, sourceRef);
|
|
860
|
+
} finally {
|
|
861
|
+
releaseHash();
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
733
865
|
/**
|
|
734
866
|
* Validates then acquires global + per-entity import locks atomically.
|
|
735
867
|
* Validates all entities before acquiring any lock (same as current importDump semantics).
|
|
@@ -1170,11 +1302,14 @@ var DEFAULT_CHUNK_OVERLAP = 400;
|
|
|
1170
1302
|
|
|
1171
1303
|
// src/services/IngestionService.ts
|
|
1172
1304
|
var IngestionService = class {
|
|
1173
|
-
constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
1305
|
+
constructor(db, prefix, options, entryRepo, sourceRefIndexRepo, metadataRepo, edgeRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
1174
1306
|
this.db = db;
|
|
1175
1307
|
this.prefix = prefix;
|
|
1176
1308
|
this.options = options;
|
|
1177
1309
|
this.entryRepo = entryRepo;
|
|
1310
|
+
this.sourceRefIndexRepo = sourceRefIndexRepo;
|
|
1311
|
+
this.metadataRepo = metadataRepo;
|
|
1312
|
+
this.edgeRepo = edgeRepo;
|
|
1178
1313
|
this.searchService = searchService;
|
|
1179
1314
|
this.jobManager = jobManager;
|
|
1180
1315
|
this.embeddingService = embeddingService;
|
|
@@ -1186,18 +1321,6 @@ var IngestionService = class {
|
|
|
1186
1321
|
if (!sourceRef) throw new Error("Invalid sourceRef");
|
|
1187
1322
|
const sourceHash = normalizeSourceHash(params.sourceHash);
|
|
1188
1323
|
if (!sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
|
|
1189
|
-
const onDuplicateHash = opts?.onDuplicateHash ?? "ingest";
|
|
1190
|
-
if (onDuplicateHash !== "ingest") {
|
|
1191
|
-
const refs = await this.entryRepo.findSourceRefsByHash(entityId, sourceHash);
|
|
1192
|
-
const others = refs.filter((r) => r !== sourceRef);
|
|
1193
|
-
if (others.length > 0) {
|
|
1194
|
-
const canonical = others[0];
|
|
1195
|
-
if (onDuplicateHash === "throw") {
|
|
1196
|
-
throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
|
|
1197
|
-
}
|
|
1198
|
-
return { truncated: false, chunks: 0, duplicateOf: canonical };
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
1324
|
const maxChunkLength = params.maxChunkLength ?? this.options.config?.maxChunkLength ?? DEFAULT_MAX_CHUNK_LENGTH;
|
|
1202
1325
|
const rawOverlap = params.chunkOverlap ?? this.options.config?.chunkOverlap ?? DEFAULT_CHUNK_OVERLAP;
|
|
1203
1326
|
const chunkOverlap = Math.min(
|
|
@@ -1209,8 +1332,18 @@ var IngestionService = class {
|
|
|
1209
1332
|
if (typeof params.documentChunk !== "string") {
|
|
1210
1333
|
throw new Error(`documentChunk must be a string, received ${typeof params.documentChunk}`);
|
|
1211
1334
|
}
|
|
1212
|
-
this.jobManager.
|
|
1335
|
+
const releaseIngestLocks = await this.jobManager.acquireIngestLocks(entityId, sourceRef, sourceHash);
|
|
1213
1336
|
try {
|
|
1337
|
+
const onDuplicateHash = opts?.onDuplicateHash ?? "ingest";
|
|
1338
|
+
if (onDuplicateHash !== "ingest") {
|
|
1339
|
+
const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash);
|
|
1340
|
+
if (canonical !== null && canonical !== sourceRef) {
|
|
1341
|
+
if (onDuplicateHash === "throw") {
|
|
1342
|
+
throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
|
|
1343
|
+
}
|
|
1344
|
+
return { truncated: false, chunks: 0, duplicateOf: canonical };
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1214
1347
|
const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
|
|
1215
1348
|
if (chunks.length === 0) return { truncated: false, chunks: 0 };
|
|
1216
1349
|
const chunkResults = await withConcurrency(
|
|
@@ -1246,68 +1379,85 @@ var IngestionService = class {
|
|
|
1246
1379
|
const now = Date.now();
|
|
1247
1380
|
const insertedFacts = [];
|
|
1248
1381
|
const deletedSourceFactIds = [];
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1382
|
+
try {
|
|
1383
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
1384
|
+
deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
|
|
1385
|
+
const titleIndex = /* @__PURE__ */ new Map();
|
|
1386
|
+
const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx, sourceRef);
|
|
1387
|
+
for (const existing of existingFacts) {
|
|
1388
|
+
titleIndex.set(normalizeTitleKey(existing.title), {
|
|
1389
|
+
id: existing.id,
|
|
1390
|
+
okf_type: existing.okf_type ?? null
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
let ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
1394
|
+
let { mode, manifest } = ontologyState;
|
|
1395
|
+
const hostNodes = [];
|
|
1396
|
+
const rawEdgeRequests = [];
|
|
1397
|
+
for (const { facts, ontology_updates } of orderedChunkFacts) {
|
|
1398
|
+
if (mode === "emergent" && ontology_updates && this.ontologyService) {
|
|
1399
|
+
manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontology_updates, tx);
|
|
1400
|
+
ontologyState = await this.ontologyService.getEffectiveState(entityId, tx);
|
|
1401
|
+
mode = ontologyState.mode;
|
|
1402
|
+
}
|
|
1403
|
+
for (const fact of facts) {
|
|
1404
|
+
const ontologyFact = fact;
|
|
1405
|
+
const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
|
|
1406
|
+
const id = generateId("fact_");
|
|
1407
|
+
hostNodes.push({
|
|
1408
|
+
id,
|
|
1409
|
+
type: ontologyFact.okf_type ?? "",
|
|
1410
|
+
title: fact.title,
|
|
1411
|
+
body: fact.body,
|
|
1412
|
+
// Forward the LLM-extracted tags/confidence through the
|
|
1413
|
+
// extracted-shape upsertGraphCore path so the entries row
|
|
1414
|
+
// stores them (search filterability, heal-candidate
|
|
1415
|
+
// selection, runReembed embedding-text signal). The public
|
|
1416
|
+
// WikiMemory.upsertGraph API doesn't accept tags/confidence
|
|
1417
|
+
// — host-supplied deterministic nodes default to [] and
|
|
1418
|
+
// 'certain' as documented.
|
|
1419
|
+
tags: fact.tags,
|
|
1420
|
+
confidence: fact.confidence
|
|
1421
|
+
});
|
|
1422
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1423
|
+
titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
|
|
1424
|
+
if (normalized.edges.length > 0) {
|
|
1425
|
+
rawEdgeRequests.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1268
1428
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
source_ref: sourceRef,
|
|
1283
|
-
created_at: now,
|
|
1284
|
-
updated_at: now,
|
|
1285
|
-
last_accessed_at: null,
|
|
1286
|
-
access_count: 0,
|
|
1287
|
-
deleted_at: null,
|
|
1288
|
-
okf_type: normalized.okf_type
|
|
1289
|
-
};
|
|
1290
|
-
await this.entryRepo.upsert(wikiFact, tx);
|
|
1291
|
-
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1292
|
-
titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
|
|
1293
|
-
if (normalized.edges.length > 0) {
|
|
1294
|
-
pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
1429
|
+
const hostEdges = [];
|
|
1430
|
+
for (const req of rawEdgeRequests) {
|
|
1431
|
+
const resolved = this.ontologyService?.resolveEdges(
|
|
1432
|
+
entityId,
|
|
1433
|
+
req.sourceId,
|
|
1434
|
+
req.sourceType,
|
|
1435
|
+
req.edges,
|
|
1436
|
+
manifest,
|
|
1437
|
+
titleIndex,
|
|
1438
|
+
now
|
|
1439
|
+
) ?? [];
|
|
1440
|
+
for (const e of resolved) {
|
|
1441
|
+
hostEdges.push({ type: e.edge_type, sourceId: e.source_id, targetId: e.target_id });
|
|
1295
1442
|
}
|
|
1296
1443
|
}
|
|
1297
|
-
|
|
1298
|
-
for (const item of pendingEdges) {
|
|
1299
|
-
await this.ontologyService?.resolveAndPersistEdges(
|
|
1444
|
+
await this.upsertGraphCore(
|
|
1300
1445
|
entityId,
|
|
1301
|
-
|
|
1302
|
-
item.sourceType,
|
|
1303
|
-
item.edges ?? [],
|
|
1304
|
-
manifest,
|
|
1305
|
-
titleIndex,
|
|
1446
|
+
{ sourceRef, sourceHash, nodes: hostNodes, edges: hostEdges },
|
|
1306
1447
|
tx,
|
|
1307
|
-
|
|
1448
|
+
{ strict: false }
|
|
1308
1449
|
);
|
|
1450
|
+
});
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
const sqliteCode = err instanceof WikiTransactionError ? err.sqliteErrorCode : extractSqliteCode(err);
|
|
1453
|
+
if (sqliteCode !== "SQLITE_CONSTRAINT_UNIQUE") throw err;
|
|
1454
|
+
const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash);
|
|
1455
|
+
if (canonical === null) throw err;
|
|
1456
|
+
if (onDuplicateHash === "throw" || onDuplicateHash === "ingest") {
|
|
1457
|
+
throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
|
|
1309
1458
|
}
|
|
1310
|
-
|
|
1459
|
+
return { truncated: false, chunks: 0, duplicateOf: canonical };
|
|
1460
|
+
}
|
|
1311
1461
|
await this.searchService.sync(entityId);
|
|
1312
1462
|
const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
|
|
1313
1463
|
for (const factId of uniqueDeletedSourceFactIds) {
|
|
@@ -1323,8 +1473,128 @@ var IngestionService = class {
|
|
|
1323
1473
|
this.searchService.evictCache(entityId);
|
|
1324
1474
|
return { truncated, chunks: chunks.length };
|
|
1325
1475
|
} finally {
|
|
1326
|
-
|
|
1476
|
+
releaseIngestLocks();
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Implements spec §2 data flow steps a–j with the host-facing
|
|
1481
|
+
* `{sourceRef, sourceHash, nodes, edges}` parameter shape. Shared between
|
|
1482
|
+
* `ingestDocument` (with `{ strict: false }` to preserve current behavior)
|
|
1483
|
+
* and `WikiMemory.upsertGraph` (which lets the persisted ontology mode
|
|
1484
|
+
* decide strictness).
|
|
1485
|
+
*
|
|
1486
|
+
* Runs ENTIRELY inside the supplied `tx`. Does not acquire locks, does not
|
|
1487
|
+
* open a nested transaction, does not perform post-commit work (search
|
|
1488
|
+
* sync, embedding, cache eviction). Those concerns remain in `ingestDocument`.
|
|
1489
|
+
*
|
|
1490
|
+
* The pre-flight validation (steps a–c) lives INSIDE this method per the
|
|
1491
|
+
* spec, so the public `WikiMemory.upsertGraph` can be a thin wrapper that
|
|
1492
|
+
* performs only the C2 probe before delegating.
|
|
1493
|
+
*
|
|
1494
|
+
* @returns Counts: nodesWritten (validated nodes persisted), edgesWritten
|
|
1495
|
+
* (manifest-valid edges persisted), superseded (prior facts soft-deleted
|
|
1496
|
+
* plus prior source-ref edges hard-deleted).
|
|
1497
|
+
*/
|
|
1498
|
+
async upsertGraphCore(entityId, params, tx, opts) {
|
|
1499
|
+
const now = Date.now();
|
|
1500
|
+
const ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
1501
|
+
let { mode, manifest } = ontologyState;
|
|
1502
|
+
const strictEffective = opts?.strict === false ? false : opts?.strict === true || mode === "strict";
|
|
1503
|
+
const wikiFacts = [];
|
|
1504
|
+
for (const node of params.nodes) {
|
|
1505
|
+
const factLike = { okf_type: node.type, edges: [] };
|
|
1506
|
+
const normalized = this.ontologyService?.validateAndNormalizeFact(factLike, manifest, {
|
|
1507
|
+
strict: strictEffective,
|
|
1508
|
+
entityId
|
|
1509
|
+
}) ?? { okf_type: null};
|
|
1510
|
+
const wikiFact = {
|
|
1511
|
+
id: node.id,
|
|
1512
|
+
entity_id: entityId,
|
|
1513
|
+
title: node.title,
|
|
1514
|
+
body: node.body ?? "",
|
|
1515
|
+
// Public upsertGraph callers don't supply tags/confidence, so they
|
|
1516
|
+
// default to [] / 'certain' for deterministic AST-grade facts.
|
|
1517
|
+
// ingestDocument routes through this method for the LLM path and
|
|
1518
|
+
// forwards the LLM-supplied tags + confidence, preserving the prior
|
|
1519
|
+
// behavior (search filterability, heal-candidate selection, runReembed
|
|
1520
|
+
// embedding-text signal).
|
|
1521
|
+
tags: node.tags ? Array.from(node.tags) : [],
|
|
1522
|
+
confidence: node.confidence ?? "certain",
|
|
1523
|
+
source_type: "immutable_document",
|
|
1524
|
+
source_hash: params.sourceHash,
|
|
1525
|
+
source_ref: params.sourceRef,
|
|
1526
|
+
created_at: now,
|
|
1527
|
+
updated_at: now,
|
|
1528
|
+
last_accessed_at: null,
|
|
1529
|
+
access_count: 0,
|
|
1530
|
+
deleted_at: null,
|
|
1531
|
+
okf_type: normalized.okf_type
|
|
1532
|
+
};
|
|
1533
|
+
wikiFacts.push(wikiFact);
|
|
1534
|
+
}
|
|
1535
|
+
const sourceIdToType = /* @__PURE__ */ new Map();
|
|
1536
|
+
for (const fact of wikiFacts) sourceIdToType.set(fact.id, fact.okf_type ?? null);
|
|
1537
|
+
const hasConstraints = (manifest.node_types?.length ?? 0) > 0 || (manifest.edge_types?.length ?? 0) > 0;
|
|
1538
|
+
const validEdges = [];
|
|
1539
|
+
for (const edge of params.edges) {
|
|
1540
|
+
if (!hasConstraints) {
|
|
1541
|
+
validEdges.push({
|
|
1542
|
+
id: edge.id ?? generateId(),
|
|
1543
|
+
entity_id: entityId,
|
|
1544
|
+
source_id: edge.sourceId,
|
|
1545
|
+
target_id: edge.targetId,
|
|
1546
|
+
edge_type: edge.type,
|
|
1547
|
+
created_at: now
|
|
1548
|
+
});
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (!sourceIdToType.has(edge.sourceId)) {
|
|
1552
|
+
validEdges.push({
|
|
1553
|
+
id: edge.id ?? generateId(),
|
|
1554
|
+
entity_id: entityId,
|
|
1555
|
+
source_id: edge.sourceId,
|
|
1556
|
+
target_id: edge.targetId,
|
|
1557
|
+
edge_type: edge.type,
|
|
1558
|
+
created_at: now
|
|
1559
|
+
});
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
const sourceType = sourceIdToType.get(edge.sourceId);
|
|
1563
|
+
const candidates = (manifest.edge_types ?? []).filter(
|
|
1564
|
+
(d) => d.type.toLowerCase() === edge.type.toLowerCase() && d.source_type.toLowerCase() === (sourceType ?? "").toLowerCase()
|
|
1565
|
+
);
|
|
1566
|
+
const match = candidates[0];
|
|
1567
|
+
if (!match) {
|
|
1568
|
+
if (strictEffective) throw new WikiStrictOntologyViolation(entityId, "edge", edge.type);
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
validEdges.push({
|
|
1572
|
+
id: edge.id ?? generateId(),
|
|
1573
|
+
entity_id: entityId,
|
|
1574
|
+
source_id: edge.sourceId,
|
|
1575
|
+
target_id: edge.targetId,
|
|
1576
|
+
edge_type: match.type,
|
|
1577
|
+
created_at: now
|
|
1578
|
+
});
|
|
1327
1579
|
}
|
|
1580
|
+
const deletedFactIds = await this.entryRepo.findIdsBySource(entityId, params.sourceRef, null, tx, false);
|
|
1581
|
+
await this.entryRepo.softDeleteBySource(entityId, tx, params.sourceRef, null);
|
|
1582
|
+
const deletedEdgeCount = await this.edgeRepo.softDeleteBySourceFactIds(entityId, deletedFactIds, tx);
|
|
1583
|
+
await this.sourceRefIndexRepo.softDeleteByEntityAndSourceRef(entityId, params.sourceRef, tx);
|
|
1584
|
+
await this.sourceRefIndexRepo.upsert(entityId, params.sourceHash, params.sourceRef, tx);
|
|
1585
|
+
for (const wikiFact of wikiFacts) {
|
|
1586
|
+
await this.entryRepo.upsert(wikiFact, tx);
|
|
1587
|
+
}
|
|
1588
|
+
let edgesInsertedCount = 0;
|
|
1589
|
+
for (const edge of validEdges) {
|
|
1590
|
+
const inserted = await this.edgeRepo.addIgnoreDuplicate(edge, tx);
|
|
1591
|
+
if (inserted) edgesInsertedCount++;
|
|
1592
|
+
}
|
|
1593
|
+
return {
|
|
1594
|
+
nodesWritten: wikiFacts.length,
|
|
1595
|
+
edgesWritten: edgesInsertedCount,
|
|
1596
|
+
superseded: deletedFactIds.length + deletedEdgeCount
|
|
1597
|
+
};
|
|
1328
1598
|
}
|
|
1329
1599
|
};
|
|
1330
1600
|
|
|
@@ -1438,16 +1708,25 @@ var MetadataRepository = class extends BaseRepository {
|
|
|
1438
1708
|
await this.db.execAsync(`PRAGMA wal_checkpoint(TRUNCATE)`);
|
|
1439
1709
|
await this.db.execAsync(`VACUUM`);
|
|
1440
1710
|
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Returns the set of distinct `entity_id` values across entries/tasks/events.
|
|
1713
|
+
*
|
|
1714
|
+
* Deliberately widened from a prior `WHERE deleted_at IS NULL` form: the
|
|
1715
|
+
* returned set intentionally includes entities whose only rows are
|
|
1716
|
+
* soft-deleted, so the host's maintenance sweep can visit such
|
|
1717
|
+
* "decommissioned-scope" namespaces and reap the soft-deleted rows. Sorting
|
|
1718
|
+
* is `entity_id COLLATE BINARY` for stable, locale-independent ordering.
|
|
1719
|
+
*/
|
|
1441
1720
|
async getDistinctEntityIds(tx) {
|
|
1442
1721
|
const executor = this.getExecutor(tx);
|
|
1443
1722
|
const rows = await executor.getAllAsync(
|
|
1444
1723
|
`SELECT DISTINCT entity_id FROM (
|
|
1445
|
-
SELECT entity_id FROM ${this.prefix}entries
|
|
1724
|
+
SELECT entity_id FROM ${this.prefix}entries
|
|
1446
1725
|
UNION
|
|
1447
|
-
SELECT entity_id FROM ${this.prefix}tasks
|
|
1726
|
+
SELECT entity_id FROM ${this.prefix}tasks
|
|
1448
1727
|
UNION
|
|
1449
1728
|
SELECT entity_id FROM ${this.prefix}events
|
|
1450
|
-
) ORDER BY entity_id`
|
|
1729
|
+
) ORDER BY entity_id COLLATE BINARY`
|
|
1451
1730
|
);
|
|
1452
1731
|
return rows.map((r) => r.entity_id);
|
|
1453
1732
|
}
|
|
@@ -1610,11 +1889,12 @@ var HEAL_MAX_PROMPT_CHARS = 4e4;
|
|
|
1610
1889
|
var HEAL_BATCH_SIZE = 25;
|
|
1611
1890
|
var HEAL_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1612
1891
|
var MaintenanceService = class {
|
|
1613
|
-
constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
1892
|
+
constructor(db, prefix, options, entryRepo, sourceRefIndexRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
1614
1893
|
this.db = db;
|
|
1615
1894
|
this.prefix = prefix;
|
|
1616
1895
|
this.options = options;
|
|
1617
1896
|
this.entryRepo = entryRepo;
|
|
1897
|
+
this.sourceRefIndexRepo = sourceRefIndexRepo;
|
|
1618
1898
|
this.taskRepo = taskRepo;
|
|
1619
1899
|
this.eventRepo = eventRepo;
|
|
1620
1900
|
this.metadataRepo = metadataRepo;
|
|
@@ -1813,10 +2093,12 @@ var MaintenanceService = class {
|
|
|
1813
2093
|
const entryPromise = params.entryId ? this.entryRepo.softDelete(params.entryId, entityId, tx).then((r) => r.changes > 0) : null;
|
|
1814
2094
|
const taskDeletedPromise = params.taskId ? this.taskRepo.softDeleteById(params.taskId, entityId, tx).then((r) => r.changes > 0) : null;
|
|
1815
2095
|
const refPromise = sourceRef || sourceHash ? this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, sourceHash) : null;
|
|
2096
|
+
const sourceRefIndexPromise = sourceRef ? this.sourceRefIndexRepo.softDeleteByEntityAndSourceRef(entityId, sourceRef, tx).then(() => void 0) : null;
|
|
1816
2097
|
const [entryResult, taskResult, refResult] = await Promise.all([
|
|
1817
2098
|
entryPromise ?? Promise.resolve(false),
|
|
1818
2099
|
taskDeletedPromise ?? Promise.resolve(false),
|
|
1819
|
-
refPromise ?? Promise.resolve(0)
|
|
2100
|
+
refPromise ?? Promise.resolve(0),
|
|
2101
|
+
sourceRefIndexPromise ?? Promise.resolve(void 0)
|
|
1820
2102
|
]);
|
|
1821
2103
|
if (entryResult) deletedEntries++;
|
|
1822
2104
|
if (taskResult) deletedTasks++;
|
|
@@ -1934,7 +2216,7 @@ var MaintenanceService = class {
|
|
|
1934
2216
|
}
|
|
1935
2217
|
if (skip) continue;
|
|
1936
2218
|
const ontologyFact = fact;
|
|
1937
|
-
const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
|
|
2219
|
+
const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
|
|
1938
2220
|
const id = generateId("fact_");
|
|
1939
2221
|
const factObj = {
|
|
1940
2222
|
id,
|
|
@@ -2319,7 +2601,8 @@ var MaintenanceService = class {
|
|
|
2319
2601
|
okf_type: classification.okf_type,
|
|
2320
2602
|
edges: classification.edges
|
|
2321
2603
|
},
|
|
2322
|
-
manifest
|
|
2604
|
+
manifest,
|
|
2605
|
+
{ strict: false }
|
|
2323
2606
|
);
|
|
2324
2607
|
if (!normalized.okf_type) {
|
|
2325
2608
|
failedValidation++;
|
|
@@ -3630,6 +3913,6 @@ var WriteService = class {
|
|
|
3630
3913
|
}
|
|
3631
3914
|
};
|
|
3632
3915
|
|
|
3633
|
-
export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, EmbeddingService, HEAL_BATCH_SIZE, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDuplicateHashError, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, validateInlineEdges, validateManifest };
|
|
3634
|
-
//# sourceMappingURL=chunk-
|
|
3635
|
-
//# sourceMappingURL=chunk-
|
|
3916
|
+
export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, EmbeddingService, HEAL_BATCH_SIZE, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDuplicateHashError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, validateInlineEdges, validateManifest };
|
|
3917
|
+
//# sourceMappingURL=chunk-KIJINNYU.mjs.map
|
|
3918
|
+
//# sourceMappingURL=chunk-KIJINNYU.mjs.map
|