@equationalapplications/core-llm-wiki 5.2.0 → 5.2.1
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-6HNIOKT4.mjs} +188 -77
- package/dist/chunk-6HNIOKT4.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +326 -85
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +142 -12
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-DyXISRwS.d.mts → testing-CAk9oFvw.d.mts} +109 -6
- package/dist/{testing-DyXISRwS.d.ts → testing-CAk9oFvw.d.ts} +109 -6
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +240 -75
- 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/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, generateId, BaseRepository, emptyManifest, resolveNodeType, validateInlineEdges, resolveEdgeDefinitions, normalizeTitleKey, WikiTransactionError, extractSqliteCode } from './chunk-
|
|
2
|
-
export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, HEAL_BATCH_SIZE, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDuplicateHashError, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-
|
|
1
|
+
import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, generateId, BaseRepository, emptyManifest, resolveNodeType, validateInlineEdges, resolveEdgeDefinitions, normalizeTitleKey, WikiTransactionError, extractSqliteCode } from './chunk-6HNIOKT4.mjs';
|
|
2
|
+
export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, HEAL_BATCH_SIZE, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDuplicateHashError, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-6HNIOKT4.mjs';
|
|
3
3
|
import { appendRelatedSection, buildConceptDocument, buildLogMd, buildEntityIndexMd, buildRootIndexMd, isAllowedOkfPath, parseRootIndexMd, parseEntityIndexMd, parseConcept, splitRelatedSection, extractMarkdownLinks, parseLogMd, parseEventIdComment, appendEventIdComment } from '@equationalapplications/core-okf';
|
|
4
4
|
|
|
5
5
|
// src/db/schema.ts
|
|
@@ -32,6 +32,24 @@ async function setupDatabase(db, prefix) {
|
|
|
32
32
|
CREATE INDEX IF NOT EXISTS ${prefix}entries_source_hash_idx ON ${prefix}entries(entity_id, source_hash) WHERE source_hash IS NOT NULL;
|
|
33
33
|
CREATE INDEX IF NOT EXISTS ${prefix}entries_updated_idx ON ${prefix}entries(updated_at DESC);
|
|
34
34
|
|
|
35
|
+
-- source_ref_index: per-(entity, source_hash) record of the canonical sourceRef
|
|
36
|
+
-- currently holding that hash. The partial UNIQUE index on (entity_id, source_hash)
|
|
37
|
+
-- WHERE deleted_at IS NULL enforces the sourceRef-level TOCTOU-race invariant;
|
|
38
|
+
-- entries-level uniqueness cannot express it because a single ingestDocument call
|
|
39
|
+
-- writes N facts that all share (entity_id, source_ref, source_hash). See
|
|
40
|
+
-- docs/superpowers/specs/2026-08-07-dependabot-concurrency-release-hygiene-design.md \xA7B1.
|
|
41
|
+
CREATE TABLE IF NOT EXISTS ${prefix}source_ref_index (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
entity_id TEXT NOT NULL,
|
|
44
|
+
source_hash TEXT NOT NULL,
|
|
45
|
+
source_ref TEXT NOT NULL,
|
|
46
|
+
created_at INTEGER NOT NULL,
|
|
47
|
+
deleted_at INTEGER
|
|
48
|
+
);
|
|
49
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ${prefix}idx_source_ref_hash
|
|
50
|
+
ON ${prefix}source_ref_index (entity_id, source_hash)
|
|
51
|
+
WHERE deleted_at IS NULL;
|
|
52
|
+
|
|
35
53
|
CREATE TABLE IF NOT EXISTS ${prefix}tasks (
|
|
36
54
|
id TEXT PRIMARY KEY,
|
|
37
55
|
entity_id TEXT NOT NULL,
|
|
@@ -278,6 +296,61 @@ var MIGRATIONS = [
|
|
|
278
296
|
);
|
|
279
297
|
}
|
|
280
298
|
}
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
version: 9,
|
|
302
|
+
description: "add_source_ref_index",
|
|
303
|
+
run: async (db, prefix) => {
|
|
304
|
+
const duplicates = await db.getAllAsync(
|
|
305
|
+
`SELECT entity_id, source_hash, COUNT(DISTINCT source_ref) AS n_refs
|
|
306
|
+
FROM ${prefix}entries
|
|
307
|
+
WHERE deleted_at IS NULL AND source_hash IS NOT NULL
|
|
308
|
+
GROUP BY entity_id, source_hash
|
|
309
|
+
HAVING COUNT(DISTINCT source_ref) > 1`
|
|
310
|
+
);
|
|
311
|
+
if (duplicates.length > 0) {
|
|
312
|
+
const sample = duplicates.slice(0, 5).map((d) => `(entity_id=${d.entity_id}, source_hash=${d.source_hash.slice(0, 12)}\u2026, n_refs=${d.n_refs})`).join(", ");
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Migration v9 (add_source_ref_index) failed: existing live rows have multiple sourceRefs sharing a hash. Found ${duplicates.length} duplicate (entity_id, source_hash) groups. First ${Math.min(5, duplicates.length)}: ${sample}. Resolve each by calling forget({ sourceRef: <loser> }) for the offending sourceRef, then re-run setup.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
await db.execAsync(
|
|
318
|
+
`CREATE TABLE IF NOT EXISTS ${prefix}source_ref_index (
|
|
319
|
+
id TEXT PRIMARY KEY,
|
|
320
|
+
entity_id TEXT NOT NULL,
|
|
321
|
+
source_hash TEXT NOT NULL,
|
|
322
|
+
source_ref TEXT NOT NULL,
|
|
323
|
+
created_at INTEGER NOT NULL,
|
|
324
|
+
deleted_at INTEGER
|
|
325
|
+
);
|
|
326
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ${prefix}idx_source_ref_hash
|
|
327
|
+
ON ${prefix}source_ref_index (entity_id, source_hash)
|
|
328
|
+
WHERE deleted_at IS NULL;`
|
|
329
|
+
);
|
|
330
|
+
await db.execAsync(
|
|
331
|
+
`INSERT OR IGNORE INTO ${prefix}source_ref_index (id, entity_id, source_hash, source_ref, created_at, deleted_at)
|
|
332
|
+
SELECT
|
|
333
|
+
'sri:' || entity_id || ':' || source_hash,
|
|
334
|
+
entity_id,
|
|
335
|
+
source_hash,
|
|
336
|
+
source_ref,
|
|
337
|
+
updated_at,
|
|
338
|
+
NULL
|
|
339
|
+
FROM (
|
|
340
|
+
SELECT
|
|
341
|
+
entity_id, source_hash, source_ref, updated_at,
|
|
342
|
+
ROW_NUMBER() OVER (
|
|
343
|
+
PARTITION BY entity_id, source_hash
|
|
344
|
+
ORDER BY updated_at ASC, id ASC
|
|
345
|
+
) AS rn
|
|
346
|
+
FROM ${prefix}entries
|
|
347
|
+
WHERE deleted_at IS NULL
|
|
348
|
+
AND source_hash IS NOT NULL
|
|
349
|
+
AND source_ref IS NOT NULL
|
|
350
|
+
) ranked
|
|
351
|
+
WHERE rn = 1;`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
281
354
|
}
|
|
282
355
|
];
|
|
283
356
|
for (let i = 1; i < MIGRATIONS.length; i++) {
|
|
@@ -1394,6 +1467,57 @@ var OutboxRepository = class extends BaseRepository {
|
|
|
1394
1467
|
}
|
|
1395
1468
|
};
|
|
1396
1469
|
|
|
1470
|
+
// src/repositories/SourceRefIndexRepository.ts
|
|
1471
|
+
var SourceRefIndexRepository = class extends BaseRepository {
|
|
1472
|
+
/**
|
|
1473
|
+
* Idempotent insert: the partial UNIQUE index catches concurrent inserts for
|
|
1474
|
+
* the same (entity_id, source_hash). The caller (IngestionService) catches
|
|
1475
|
+
* the resulting SQLITE_CONSTRAINT_UNIQUE and translates it to the per-mode
|
|
1476
|
+
* duplicate-hash outcome. Runtime IDs use the `sri_` prefix to avoid
|
|
1477
|
+
* collision with the deterministic `sri:<entity>:<hash>` IDs used by the v9
|
|
1478
|
+
* backfill (the colon separator keeps the two ID spaces disjoint).
|
|
1479
|
+
*/
|
|
1480
|
+
async upsert(entityId, sourceHash, sourceRef, tx) {
|
|
1481
|
+
const executor = this.getExecutor(tx);
|
|
1482
|
+
await executor.runAsync(
|
|
1483
|
+
`INSERT INTO ${this.prefix}source_ref_index (id, entity_id, source_hash, source_ref, created_at, deleted_at)
|
|
1484
|
+
VALUES (?, ?, ?, ?, ?, NULL)`,
|
|
1485
|
+
[generateId("sri_"), entityId, sourceHash, sourceRef, Date.now()]
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Idempotent soft-delete of the live row for (entity_id, source_ref).
|
|
1490
|
+
* Called at the start of every ingestDocument to remove the prior run's
|
|
1491
|
+
* index row, so the new upsert doesn't collide with itself. No-op when the
|
|
1492
|
+
* row is already soft-deleted or never existed.
|
|
1493
|
+
*/
|
|
1494
|
+
async softDeleteByEntityAndSourceRef(entityId, sourceRef, tx) {
|
|
1495
|
+
const executor = this.getExecutor(tx);
|
|
1496
|
+
const now = Date.now();
|
|
1497
|
+
const result = await executor.runAsync(
|
|
1498
|
+
`UPDATE ${this.prefix}source_ref_index
|
|
1499
|
+
SET deleted_at = ?, created_at = ?
|
|
1500
|
+
WHERE entity_id = ? AND source_ref = ? AND deleted_at IS NULL`,
|
|
1501
|
+
[now, now, entityId, sourceRef]
|
|
1502
|
+
);
|
|
1503
|
+
return result.changes;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Returns the live sourceRef holding the given hash, or null when no live
|
|
1507
|
+
* row exists. Used by the IngestionService pre-check (line 82) and the
|
|
1508
|
+
* catch-and-translate canonical lookup (line 212).
|
|
1509
|
+
*/
|
|
1510
|
+
async findActiveByEntityAndHash(entityId, sourceHash, tx) {
|
|
1511
|
+
const executor = this.getExecutor(tx);
|
|
1512
|
+
const row = await executor.getFirstAsync(
|
|
1513
|
+
`SELECT source_ref FROM ${this.prefix}source_ref_index
|
|
1514
|
+
WHERE entity_id = ? AND source_hash = ? AND deleted_at IS NULL`,
|
|
1515
|
+
[entityId, sourceHash]
|
|
1516
|
+
);
|
|
1517
|
+
return row?.source_ref ?? null;
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1397
1521
|
// src/repositories/TaskRepository.ts
|
|
1398
1522
|
function mapRowToTask(row) {
|
|
1399
1523
|
return {
|
|
@@ -2070,6 +2194,7 @@ var WikiMemory = class {
|
|
|
2070
2194
|
}
|
|
2071
2195
|
this.outboxRepo = new OutboxRepository(this.db, this.prefix, !!options.config?.enableOutbox);
|
|
2072
2196
|
this.entryRepo = new EntryRepository(this.db, this.prefix, this.outboxRepo);
|
|
2197
|
+
this.sourceRefIndexRepo = new SourceRefIndexRepository(this.db, this.prefix);
|
|
2073
2198
|
this.taskRepo = new TaskRepository(this.db, this.prefix, this.outboxRepo);
|
|
2074
2199
|
this.eventRepo = new EventRepository(this.db, this.prefix);
|
|
2075
2200
|
this.edgeRepo = new EdgeRepository(this.db, this.prefix);
|
|
@@ -2088,6 +2213,7 @@ var WikiMemory = class {
|
|
|
2088
2213
|
this.prefix,
|
|
2089
2214
|
this.options,
|
|
2090
2215
|
this.entryRepo,
|
|
2216
|
+
this.sourceRefIndexRepo,
|
|
2091
2217
|
this.searchService,
|
|
2092
2218
|
this.jobManager,
|
|
2093
2219
|
this.embeddingService,
|
|
@@ -2099,6 +2225,7 @@ var WikiMemory = class {
|
|
|
2099
2225
|
this.prefix,
|
|
2100
2226
|
this.options,
|
|
2101
2227
|
this.entryRepo,
|
|
2228
|
+
this.sourceRefIndexRepo,
|
|
2102
2229
|
this.taskRepo,
|
|
2103
2230
|
this.eventRepo,
|
|
2104
2231
|
this.metadataRepo,
|
|
@@ -2163,6 +2290,7 @@ var WikiMemory = class {
|
|
|
2163
2290
|
promptService: this.promptService,
|
|
2164
2291
|
graphTraversalService: this.graphTraversalService,
|
|
2165
2292
|
entryRepo: this.entryRepo,
|
|
2293
|
+
sourceRefIndexRepo: this.sourceRefIndexRepo,
|
|
2166
2294
|
metadataRepo: this.metadataRepo,
|
|
2167
2295
|
jobManager: this.jobManager
|
|
2168
2296
|
};
|
|
@@ -2239,7 +2367,7 @@ var WikiMemory = class {
|
|
|
2239
2367
|
const latestHashes = await this.entryRepo.findLatestSourceHashes(entityId, normalized.map((e) => e.sourceRef));
|
|
2240
2368
|
const distinctHashes = Array.from(new Set(normalized.map((e) => e.sourceHash)));
|
|
2241
2369
|
const dupRefs = await Promise.all(
|
|
2242
|
-
distinctHashes.map((h) => this.
|
|
2370
|
+
distinctHashes.map((h) => this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, h))
|
|
2243
2371
|
);
|
|
2244
2372
|
const dupMap = /* @__PURE__ */ new Map();
|
|
2245
2373
|
for (let i = 0; i < distinctHashes.length; i++) {
|
|
@@ -2248,12 +2376,10 @@ var WikiMemory = class {
|
|
|
2248
2376
|
return normalized.map((e) => {
|
|
2249
2377
|
const stored = latestHashes.get(e.sourceRef);
|
|
2250
2378
|
const changed = stored === void 0 || stored === null || normalizeSourceHash(stored) !== e.sourceHash;
|
|
2251
|
-
const
|
|
2252
|
-
|
|
2253
|
-
if (others.length === 0) {
|
|
2379
|
+
const canonical = dupMap.get(e.sourceHash) ?? null;
|
|
2380
|
+
if (canonical === null || canonical === e.sourceRef) {
|
|
2254
2381
|
return { sourceRef: e.rawSourceRef, changed };
|
|
2255
2382
|
}
|
|
2256
|
-
const canonical = others[0];
|
|
2257
2383
|
return { sourceRef: e.rawSourceRef, changed, duplicateOf: canonical };
|
|
2258
2384
|
});
|
|
2259
2385
|
}
|
|
@@ -2268,13 +2394,17 @@ var WikiMemory = class {
|
|
|
2268
2394
|
return this.entryRepo.listSourceRefs(entityId);
|
|
2269
2395
|
}
|
|
2270
2396
|
/**
|
|
2271
|
-
* Returns the live source_refs for an entity that hold the given source_hash
|
|
2272
|
-
*
|
|
2273
|
-
*
|
|
2274
|
-
*
|
|
2397
|
+
* Returns the live source_refs for an entity that hold the given source_hash.
|
|
2398
|
+
* With v9, source_ref_index is the source of truth for the sourceRef-level
|
|
2399
|
+
* TOCTOU-race invariant: at most one sourceRef can hold a given
|
|
2400
|
+
* (entity_id, source_hash). The result is either a single-element array
|
|
2401
|
+
* (one canonical ref) or empty (no live ref holds the hash). Returned as
|
|
2402
|
+
* an array to preserve the existing public-API shape used by hosts
|
|
2403
|
+
* auditing duplicate-content collisions.
|
|
2275
2404
|
*/
|
|
2276
2405
|
async findSourceRefsByHash(entityId, sourceHash) {
|
|
2277
|
-
|
|
2406
|
+
const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash);
|
|
2407
|
+
return canonical === null ? [] : [canonical];
|
|
2278
2408
|
}
|
|
2279
2409
|
async runPrune(entityId, options) {
|
|
2280
2410
|
return this.maintenanceService.runPrune(entityId, options);
|