@equationalapplications/core-llm-wiki 4.15.3 → 4.17.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 +96 -0
- package/dist/{chunk-J4GBC6CP.mjs → chunk-M74FCWLB.mjs} +238 -46
- package/dist/chunk-M74FCWLB.mjs.map +1 -0
- package/dist/index.d.mts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +621 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +388 -17
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-NH1_Aigh.d.mts → testing-D02cdI9A.d.mts} +194 -16
- package/dist/{testing-NH1_Aigh.d.ts → testing-D02cdI9A.d.ts} +194 -16
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +155 -43
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-J4GBC6CP.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -87,6 +87,13 @@ async function setupDatabase(db, prefix) {
|
|
|
87
87
|
memory_checkpoint INTEGER NOT NULL DEFAULT 0
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
+
CREATE TABLE IF NOT EXISTS ${prefix}entity_manifests (
|
|
91
|
+
entity_id TEXT PRIMARY KEY,
|
|
92
|
+
mode TEXT NOT NULL DEFAULT 'off',
|
|
93
|
+
manifest_json TEXT NOT NULL DEFAULT '{"node_types":[],"edge_types":[]}',
|
|
94
|
+
updated_at INTEGER NOT NULL
|
|
95
|
+
);
|
|
96
|
+
|
|
90
97
|
CREATE TABLE IF NOT EXISTS ${prefix}meta (
|
|
91
98
|
key TEXT PRIMARY KEY,
|
|
92
99
|
value TEXT NOT NULL
|
|
@@ -196,6 +203,20 @@ var MIGRATIONS = [
|
|
|
196
203
|
CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_idx ON ${prefix}edges (entity_id);
|
|
197
204
|
`);
|
|
198
205
|
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
version: 6,
|
|
209
|
+
description: "Add entity_manifests table for per-entity ontology state",
|
|
210
|
+
run: async (db, prefix) => {
|
|
211
|
+
await db.execAsync(`
|
|
212
|
+
CREATE TABLE IF NOT EXISTS ${prefix}entity_manifests (
|
|
213
|
+
entity_id TEXT PRIMARY KEY,
|
|
214
|
+
mode TEXT NOT NULL DEFAULT 'off',
|
|
215
|
+
manifest_json TEXT NOT NULL DEFAULT '{"node_types":[],"edge_types":[]}',
|
|
216
|
+
updated_at INTEGER NOT NULL
|
|
217
|
+
);
|
|
218
|
+
`);
|
|
219
|
+
}
|
|
199
220
|
}
|
|
200
221
|
];
|
|
201
222
|
for (let i = 1; i < MIGRATIONS.length; i++) {
|
|
@@ -319,8 +340,8 @@ var EntryRepository = class extends BaseRepository {
|
|
|
319
340
|
`INSERT INTO ${this.prefix}entries (
|
|
320
341
|
id, entity_id, title, body, tags, confidence, source_type,
|
|
321
342
|
source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count,
|
|
322
|
-
deleted_at, embedding_blob, embedding
|
|
323
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
343
|
+
deleted_at, embedding_blob, embedding, okf_type
|
|
344
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
324
345
|
ON CONFLICT(id) DO UPDATE SET
|
|
325
346
|
entity_id = excluded.entity_id,
|
|
326
347
|
title = excluded.title,
|
|
@@ -335,7 +356,8 @@ var EntryRepository = class extends BaseRepository {
|
|
|
335
356
|
access_count = excluded.access_count,
|
|
336
357
|
deleted_at = excluded.deleted_at,
|
|
337
358
|
embedding_blob = CASE WHEN excluded.embedding_blob IS NULL THEN embedding_blob ELSE excluded.embedding_blob END,
|
|
338
|
-
embedding = NULL
|
|
359
|
+
embedding = NULL,
|
|
360
|
+
okf_type = excluded.okf_type`,
|
|
339
361
|
[
|
|
340
362
|
fact.id,
|
|
341
363
|
fact.entity_id,
|
|
@@ -352,7 +374,8 @@ var EntryRepository = class extends BaseRepository {
|
|
|
352
374
|
fact.access_count,
|
|
353
375
|
fact.deleted_at ?? null,
|
|
354
376
|
embeddingBlob ?? null,
|
|
355
|
-
null
|
|
377
|
+
null,
|
|
378
|
+
fact.okf_type ?? null
|
|
356
379
|
]
|
|
357
380
|
);
|
|
358
381
|
await this.outbox.push({
|
|
@@ -1384,6 +1407,11 @@ var EventRepository = class extends BaseRepository {
|
|
|
1384
1407
|
};
|
|
1385
1408
|
|
|
1386
1409
|
// src/repositories/EdgeRepository.ts
|
|
1410
|
+
var CONFIDENCE_RANK = {
|
|
1411
|
+
tentative: 0,
|
|
1412
|
+
inferred: 1,
|
|
1413
|
+
certain: 2
|
|
1414
|
+
};
|
|
1387
1415
|
var EdgeRepository = class extends BaseRepository {
|
|
1388
1416
|
/**
|
|
1389
1417
|
* Insert an edge, silently skipping on primary-key or uniqueness conflicts.
|
|
@@ -1414,21 +1442,196 @@ var EdgeRepository = class extends BaseRepository {
|
|
|
1414
1442
|
`SELECT * FROM ${this.prefix}edges WHERE entity_id = ? ORDER BY created_at ASC`,
|
|
1415
1443
|
[entityId]
|
|
1416
1444
|
);
|
|
1417
|
-
return rows.map(
|
|
1418
|
-
id: String(row.id),
|
|
1419
|
-
entity_id: String(row.entity_id),
|
|
1420
|
-
source_id: String(row.source_id),
|
|
1421
|
-
target_id: String(row.target_id),
|
|
1422
|
-
edge_type: String(row.edge_type),
|
|
1423
|
-
created_at: Number(row.created_at)
|
|
1424
|
-
}));
|
|
1445
|
+
return rows.map(mapRowToEdge);
|
|
1425
1446
|
}
|
|
1426
1447
|
/** Hard delete — edges have no soft-delete concept, only presence/absence. `tx` is REQUIRED. */
|
|
1427
1448
|
async bulkDeleteByEntityId(entityId, tx) {
|
|
1428
1449
|
const executor = this.getExecutor(tx);
|
|
1429
1450
|
await executor.runAsync(`DELETE FROM ${this.prefix}edges WHERE entity_id = ?`, [entityId]);
|
|
1430
1451
|
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Multi-hop traversal from `sourceId` via SQLite `WITH RECURSIVE`. All filtering,
|
|
1454
|
+
* dead-ending, cycle-guarding, capping, and ordering happens in this one query.
|
|
1455
|
+
* The anchor is validated (exists, right entity, not soft-deleted) but never gated
|
|
1456
|
+
* by confidence/source_type — only nodes discovered beyond it are.
|
|
1457
|
+
*/
|
|
1458
|
+
async getNeighborhood(entityId, sourceId, opts, tx) {
|
|
1459
|
+
const executor = this.getExecutor(tx);
|
|
1460
|
+
if (opts.edgeTypes && opts.edgeTypes.length === 0) {
|
|
1461
|
+
const anchor = await executor.getFirstAsync(
|
|
1462
|
+
`SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`,
|
|
1463
|
+
[sourceId, entityId]
|
|
1464
|
+
);
|
|
1465
|
+
return { nodeIds: anchor ? [anchor.id] : [], edges: [] };
|
|
1466
|
+
}
|
|
1467
|
+
const edgeTypesClause = opts.edgeTypes ? `e.edge_type IN (${opts.edgeTypes.map(() => "?").join(",")})` : "1=1";
|
|
1468
|
+
const excludeSourceTypesPlaceholders = opts.excludeSourceTypes.map(() => "?").join(",");
|
|
1469
|
+
const minConfidenceRank = CONFIDENCE_RANK[opts.minConfidence];
|
|
1470
|
+
const sql = `
|
|
1471
|
+
WITH RECURSIVE walk(node_id, depth, visited) AS (
|
|
1472
|
+
SELECT id, 0, ',' || id || ','
|
|
1473
|
+
FROM ${this.prefix}entries
|
|
1474
|
+
WHERE id = ? AND entity_id = ? AND deleted_at IS NULL
|
|
1475
|
+
|
|
1476
|
+
UNION
|
|
1477
|
+
|
|
1478
|
+
SELECT
|
|
1479
|
+
CASE WHEN e.source_id = w.node_id THEN e.target_id ELSE e.source_id END,
|
|
1480
|
+
w.depth + 1,
|
|
1481
|
+
w.visited || (CASE WHEN e.source_id = w.node_id THEN e.target_id ELSE e.source_id END) || ','
|
|
1482
|
+
FROM walk w
|
|
1483
|
+
JOIN ${this.prefix}edges e
|
|
1484
|
+
ON e.entity_id = ?
|
|
1485
|
+
AND (
|
|
1486
|
+
(? != 'inbound' AND e.source_id = w.node_id) OR
|
|
1487
|
+
(? != 'outbound' AND e.target_id = w.node_id)
|
|
1488
|
+
)
|
|
1489
|
+
AND (${edgeTypesClause})
|
|
1490
|
+
JOIN ${this.prefix}entries n
|
|
1491
|
+
ON n.id = (CASE WHEN e.source_id = w.node_id THEN e.target_id ELSE e.source_id END)
|
|
1492
|
+
AND n.entity_id = ?
|
|
1493
|
+
AND n.deleted_at IS NULL
|
|
1494
|
+
AND (
|
|
1495
|
+
CASE n.confidence
|
|
1496
|
+
WHEN 'tentative' THEN 0
|
|
1497
|
+
WHEN 'inferred' THEN 1
|
|
1498
|
+
WHEN 'certain' THEN 2
|
|
1499
|
+
ELSE -1
|
|
1500
|
+
END
|
|
1501
|
+
) >= ?
|
|
1502
|
+
AND n.source_type NOT IN (${excludeSourceTypesPlaceholders})
|
|
1503
|
+
WHERE w.depth < ?
|
|
1504
|
+
AND instr(w.visited, ',' || (CASE WHEN e.source_id = w.node_id THEN e.target_id ELSE e.source_id END) || ',') = 0
|
|
1505
|
+
)
|
|
1506
|
+
SELECT node_id, MIN(depth) AS depth
|
|
1507
|
+
FROM walk
|
|
1508
|
+
GROUP BY node_id
|
|
1509
|
+
ORDER BY depth ASC, (SELECT updated_at FROM ${this.prefix}entries WHERE id = node_id) DESC
|
|
1510
|
+
LIMIT ?
|
|
1511
|
+
`;
|
|
1512
|
+
const params = [
|
|
1513
|
+
sourceId,
|
|
1514
|
+
entityId,
|
|
1515
|
+
entityId,
|
|
1516
|
+
opts.direction,
|
|
1517
|
+
opts.direction,
|
|
1518
|
+
...opts.edgeTypes ?? [],
|
|
1519
|
+
entityId,
|
|
1520
|
+
minConfidenceRank,
|
|
1521
|
+
...opts.excludeSourceTypes,
|
|
1522
|
+
opts.maxDepth,
|
|
1523
|
+
opts.maxNodes
|
|
1524
|
+
];
|
|
1525
|
+
const rows = await executor.getAllAsync(sql, params);
|
|
1526
|
+
const nodeIds = rows.map((r) => r.node_id);
|
|
1527
|
+
if (nodeIds.length === 0) return { nodeIds: [], edges: [] };
|
|
1528
|
+
const valueRows = nodeIds.map(() => "(?)").join(", ");
|
|
1529
|
+
const edgeRows = await executor.getAllAsync(
|
|
1530
|
+
`WITH neighborhood(node_id) AS (VALUES ${valueRows})
|
|
1531
|
+
SELECT e.* FROM ${this.prefix}edges e
|
|
1532
|
+
JOIN neighborhood ns ON e.source_id = ns.node_id
|
|
1533
|
+
JOIN neighborhood nt ON e.target_id = nt.node_id
|
|
1534
|
+
WHERE e.entity_id = ?`,
|
|
1535
|
+
[...nodeIds, entityId]
|
|
1536
|
+
);
|
|
1537
|
+
return { nodeIds, edges: edgeRows.map(mapRowToEdge) };
|
|
1538
|
+
}
|
|
1431
1539
|
};
|
|
1540
|
+
function mapRowToEdge(row) {
|
|
1541
|
+
return {
|
|
1542
|
+
id: String(row.id),
|
|
1543
|
+
entity_id: String(row.entity_id),
|
|
1544
|
+
source_id: String(row.source_id),
|
|
1545
|
+
target_id: String(row.target_id),
|
|
1546
|
+
edge_type: String(row.edge_type),
|
|
1547
|
+
created_at: Number(row.created_at)
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/utils/ontology.ts
|
|
1552
|
+
function emptyManifest() {
|
|
1553
|
+
return { node_types: [], edge_types: [] };
|
|
1554
|
+
}
|
|
1555
|
+
function normalizeTitleKey(title) {
|
|
1556
|
+
return title.trim().toLowerCase().replace(/\s+/g, " ");
|
|
1557
|
+
}
|
|
1558
|
+
function resolveNodeType(raw, manifest) {
|
|
1559
|
+
const slug = raw.trim();
|
|
1560
|
+
if (!slug) return null;
|
|
1561
|
+
const hit = manifest.node_types.find((n) => n.type.toLowerCase() === slug.toLowerCase());
|
|
1562
|
+
return hit?.type ?? null;
|
|
1563
|
+
}
|
|
1564
|
+
function resolveEdgeDefinition(rawEdgeType, manifest) {
|
|
1565
|
+
const slug = rawEdgeType.trim();
|
|
1566
|
+
if (!slug) return null;
|
|
1567
|
+
return manifest.edge_types.find((e) => e.type.toLowerCase() === slug.toLowerCase()) ?? null;
|
|
1568
|
+
}
|
|
1569
|
+
function validateManifest(manifest) {
|
|
1570
|
+
const nodeSlugs = /* @__PURE__ */ new Set();
|
|
1571
|
+
for (const node of manifest.node_types ?? []) {
|
|
1572
|
+
const type = node.type?.trim();
|
|
1573
|
+
if (!type) throw new Error("Ontology node type slug must be non-empty");
|
|
1574
|
+
const key = type.toLowerCase();
|
|
1575
|
+
if (nodeSlugs.has(key)) throw new Error(`Duplicate node type: ${type}`);
|
|
1576
|
+
nodeSlugs.add(key);
|
|
1577
|
+
}
|
|
1578
|
+
const edgeSlugs = /* @__PURE__ */ new Set();
|
|
1579
|
+
for (const edge of manifest.edge_types ?? []) {
|
|
1580
|
+
const edgeType = edge.type?.trim();
|
|
1581
|
+
const sourceType = edge.source_type?.trim();
|
|
1582
|
+
const targetType = edge.target_type?.trim();
|
|
1583
|
+
if (!edgeType) throw new Error("Ontology edge type slug must be non-empty");
|
|
1584
|
+
const edgeKey = edgeType.toLowerCase();
|
|
1585
|
+
if (edgeSlugs.has(edgeKey)) throw new Error(`Duplicate edge type: ${edgeType}`);
|
|
1586
|
+
edgeSlugs.add(edgeKey);
|
|
1587
|
+
if (!sourceType || !targetType || !nodeSlugs.has(sourceType.toLowerCase()) || !nodeSlugs.has(targetType.toLowerCase())) {
|
|
1588
|
+
throw new Error(`Edge type ${edgeType} references unknown node type`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function mergeOntologyUpdates(current, updates) {
|
|
1593
|
+
const node_types = [...current.node_types];
|
|
1594
|
+
const edge_types = [...current.edge_types];
|
|
1595
|
+
const nodeSlugs = new Set(node_types.map((n) => n.type.trim().toLowerCase()));
|
|
1596
|
+
const edgeSlugs = new Set(edge_types.map((e) => e.type.trim().toLowerCase()));
|
|
1597
|
+
for (const node of updates.node_types ?? []) {
|
|
1598
|
+
const type = node?.type?.trim();
|
|
1599
|
+
if (!type) continue;
|
|
1600
|
+
const key = type.toLowerCase();
|
|
1601
|
+
if (nodeSlugs.has(key)) continue;
|
|
1602
|
+
node_types.push({ type, description: String(node.description ?? "") });
|
|
1603
|
+
nodeSlugs.add(key);
|
|
1604
|
+
}
|
|
1605
|
+
for (const edge of updates.edge_types ?? []) {
|
|
1606
|
+
const edgeType = edge?.type?.trim();
|
|
1607
|
+
const sourceType = edge?.source_type?.trim();
|
|
1608
|
+
const targetType = edge?.target_type?.trim();
|
|
1609
|
+
if (!edgeType || !sourceType || !targetType) continue;
|
|
1610
|
+
const edgeKey = edgeType.toLowerCase();
|
|
1611
|
+
if (edgeSlugs.has(edgeKey)) continue;
|
|
1612
|
+
if (!nodeSlugs.has(sourceType.toLowerCase()) || !nodeSlugs.has(targetType.toLowerCase())) continue;
|
|
1613
|
+
edge_types.push({
|
|
1614
|
+
type: edgeType,
|
|
1615
|
+
source_type: sourceType,
|
|
1616
|
+
target_type: targetType,
|
|
1617
|
+
description: String(edge.description ?? "")
|
|
1618
|
+
});
|
|
1619
|
+
edgeSlugs.add(edgeKey);
|
|
1620
|
+
}
|
|
1621
|
+
return { node_types, edge_types };
|
|
1622
|
+
}
|
|
1623
|
+
function validateInlineEdges(sourceType, _targetType, edges, manifest) {
|
|
1624
|
+
if (!Array.isArray(edges)) return [];
|
|
1625
|
+
const valid = [];
|
|
1626
|
+
for (const edge of edges) {
|
|
1627
|
+
if (typeof edge?.edge_type !== "string" || typeof edge?.target_title !== "string") continue;
|
|
1628
|
+
const def = resolveEdgeDefinition(edge.edge_type, manifest);
|
|
1629
|
+
if (!def) continue;
|
|
1630
|
+
if (def.source_type.toLowerCase() !== sourceType.toLowerCase()) continue;
|
|
1631
|
+
valid.push({ edge_type: def.type, target_title: edge.target_title });
|
|
1632
|
+
}
|
|
1633
|
+
return valid;
|
|
1634
|
+
}
|
|
1432
1635
|
|
|
1433
1636
|
// src/repositories/MetadataRepository.ts
|
|
1434
1637
|
var MetadataRepository = class extends BaseRepository {
|
|
@@ -1528,6 +1731,44 @@ var MetadataRepository = class extends BaseRepository {
|
|
|
1528
1731
|
);
|
|
1529
1732
|
return rows.map((r) => r.entity_id);
|
|
1530
1733
|
}
|
|
1734
|
+
async getManifest(entityId, tx) {
|
|
1735
|
+
const executor = this.getExecutor(tx);
|
|
1736
|
+
const row = await executor.getFirstAsync(`SELECT mode, manifest_json FROM ${this.prefix}entity_manifests WHERE entity_id = ?`, [entityId]);
|
|
1737
|
+
if (!row) return null;
|
|
1738
|
+
if (row.mode !== "off" && row.mode !== "strict" && row.mode !== "emergent") {
|
|
1739
|
+
throw new Error(`Invalid ontology mode for entity ${entityId}: ${JSON.stringify(row.mode)}`);
|
|
1740
|
+
}
|
|
1741
|
+
let manifest;
|
|
1742
|
+
try {
|
|
1743
|
+
manifest = JSON.parse(row.manifest_json);
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
throw new Error(`Invalid manifest_json for entity ${entityId}: ${error.message}`);
|
|
1746
|
+
}
|
|
1747
|
+
validateManifest(manifest);
|
|
1748
|
+
return {
|
|
1749
|
+
mode: row.mode,
|
|
1750
|
+
manifest
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
async setManifest(entityId, data, tx) {
|
|
1754
|
+
validateManifest(data.manifest);
|
|
1755
|
+
const executor = this.getExecutor(tx);
|
|
1756
|
+
await executor.runAsync(
|
|
1757
|
+
`INSERT INTO ${this.prefix}entity_manifests (entity_id, mode, manifest_json, updated_at)
|
|
1758
|
+
VALUES (?, ?, ?, ?)
|
|
1759
|
+
ON CONFLICT(entity_id) DO UPDATE SET mode = excluded.mode, manifest_json = excluded.manifest_json, updated_at = excluded.updated_at`,
|
|
1760
|
+
[entityId, data.mode, JSON.stringify(data.manifest), Date.now()]
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
async mergeManifestUpdates(entityId, updates, tx) {
|
|
1764
|
+
const current = await this.getManifest(entityId, tx) ?? {
|
|
1765
|
+
mode: "emergent",
|
|
1766
|
+
manifest: emptyManifest()
|
|
1767
|
+
};
|
|
1768
|
+
const merged = mergeOntologyUpdates(current.manifest, updates);
|
|
1769
|
+
await this.setManifest(entityId, { mode: current.mode, manifest: merged }, tx);
|
|
1770
|
+
return merged;
|
|
1771
|
+
}
|
|
1531
1772
|
};
|
|
1532
1773
|
|
|
1533
1774
|
// src/utils/cosine.ts
|
|
@@ -2343,30 +2584,54 @@ var PromptService = class {
|
|
|
2343
2584
|
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
2344
2585
|
});
|
|
2345
2586
|
}
|
|
2346
|
-
|
|
2587
|
+
hasOntologyPlaceholders(template) {
|
|
2588
|
+
return /\{\{\s*ontology(?:Manifest|ModeInstructions)\s*\}\}/.test(template);
|
|
2589
|
+
}
|
|
2590
|
+
buildSystemPrompt(template, variables, ontologyContext) {
|
|
2591
|
+
const shouldHydrate = Object.keys(variables).some(
|
|
2592
|
+
(key) => new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`).test(template)
|
|
2593
|
+
) || ontologyContext != null && this.hasOntologyPlaceholders(template);
|
|
2594
|
+
const hydrated = shouldHydrate ? this.hydrate(template, { ...variables, ...ontologyContext ?? {} }) : template;
|
|
2595
|
+
return this.hasOntologyPlaceholders(template) ? ontologyContext != null ? hydrated : hydrated.replace(/\{\{\s*ontology(?:Manifest|ModeInstructions)\s*\}\}/g, "") : this.appendOntology(hydrated, ontologyContext);
|
|
2596
|
+
}
|
|
2597
|
+
appendOntology(systemPrompt, ctx) {
|
|
2598
|
+
if (!ctx) return systemPrompt;
|
|
2599
|
+
return `${systemPrompt}
|
|
2600
|
+
|
|
2601
|
+
${ctx.ontologyModeInstructions}`;
|
|
2602
|
+
}
|
|
2603
|
+
buildIngestPrompt(documentChunk, runtimeOverride, ontologyContext) {
|
|
2347
2604
|
const template = runtimeOverride ?? this.globalOverrides?.ingestSystemPrompt ?? INGEST_SYSTEM_PROMPT;
|
|
2348
|
-
|
|
2605
|
+
const hasDocumentChunk = /\{\{\s*documentChunk\s*\}\}/.test(template);
|
|
2606
|
+
if (hasDocumentChunk || this.hasOntologyPlaceholders(template)) {
|
|
2349
2607
|
return {
|
|
2350
|
-
systemPrompt: this.
|
|
2351
|
-
userPrompt: "Please extract the facts."
|
|
2608
|
+
systemPrompt: this.buildSystemPrompt(template, { documentChunk }, ontologyContext),
|
|
2609
|
+
userPrompt: hasDocumentChunk ? "Please extract the facts." : `Document Chunk:
|
|
2610
|
+
${documentChunk}`
|
|
2352
2611
|
};
|
|
2353
2612
|
}
|
|
2354
2613
|
return {
|
|
2355
|
-
systemPrompt: template,
|
|
2614
|
+
systemPrompt: this.appendOntology(template, ontologyContext),
|
|
2356
2615
|
userPrompt: `Document Chunk:
|
|
2357
2616
|
${documentChunk}`
|
|
2358
2617
|
};
|
|
2359
2618
|
}
|
|
2360
|
-
buildLibrarianPrompt(events, currentFacts, runtimeOverride) {
|
|
2619
|
+
buildLibrarianPrompt(events, currentFacts, runtimeOverride, ontologyContext) {
|
|
2361
2620
|
const template = runtimeOverride ?? this.globalOverrides?.librarianSystemPrompt ?? LIBRARIAN_SYSTEM_PROMPT;
|
|
2362
|
-
|
|
2621
|
+
const hasEvents = /\{\{\s*events\s*\}\}/.test(template);
|
|
2622
|
+
const hasCurrentFacts = /\{\{\s*currentFacts\s*\}\}/.test(template);
|
|
2623
|
+
if (hasEvents || hasCurrentFacts || this.hasOntologyPlaceholders(template)) {
|
|
2363
2624
|
return {
|
|
2364
|
-
systemPrompt: this.
|
|
2365
|
-
userPrompt: "Please synthesize the context."
|
|
2625
|
+
systemPrompt: this.buildSystemPrompt(template, { events, currentFacts }, ontologyContext),
|
|
2626
|
+
userPrompt: hasEvents || hasCurrentFacts ? "Please synthesize the context." : `Events:
|
|
2627
|
+
${JSON.stringify(events, null, 2)}
|
|
2628
|
+
|
|
2629
|
+
Current Facts:
|
|
2630
|
+
${JSON.stringify(currentFacts, null, 2)}`
|
|
2366
2631
|
};
|
|
2367
2632
|
}
|
|
2368
2633
|
return {
|
|
2369
|
-
systemPrompt: template,
|
|
2634
|
+
systemPrompt: this.appendOntology(template, ontologyContext),
|
|
2370
2635
|
userPrompt: `Events:
|
|
2371
2636
|
${JSON.stringify(events, null, 2)}
|
|
2372
2637
|
|
|
@@ -2399,7 +2664,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2399
2664
|
|
|
2400
2665
|
// src/services/IngestionService.ts
|
|
2401
2666
|
var IngestionService = class {
|
|
2402
|
-
constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService) {
|
|
2667
|
+
constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
2403
2668
|
this.db = db;
|
|
2404
2669
|
this.prefix = prefix;
|
|
2405
2670
|
this.options = options;
|
|
@@ -2407,6 +2672,7 @@ var IngestionService = class {
|
|
|
2407
2672
|
this.searchService = searchService;
|
|
2408
2673
|
this.jobManager = jobManager;
|
|
2409
2674
|
this.embeddingService = embeddingService;
|
|
2675
|
+
this.ontologyService = ontologyService;
|
|
2410
2676
|
this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
|
|
2411
2677
|
}
|
|
2412
2678
|
async ingestDocument(entityId, params) {
|
|
@@ -2431,23 +2697,33 @@ var IngestionService = class {
|
|
|
2431
2697
|
if (chunks.length === 0) return { truncated: false, chunks: 0 };
|
|
2432
2698
|
const chunkResults = await withConcurrency(
|
|
2433
2699
|
chunks.map((chunk) => async () => {
|
|
2434
|
-
const
|
|
2700
|
+
const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
|
|
2701
|
+
const { systemPrompt, userPrompt } = this.promptService.buildIngestPrompt(
|
|
2702
|
+
chunk,
|
|
2703
|
+
params.promptOverride,
|
|
2704
|
+
ontologyContext
|
|
2705
|
+
);
|
|
2435
2706
|
const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
|
|
2436
2707
|
const result = parseJsonResponse(responseText);
|
|
2437
|
-
return
|
|
2708
|
+
return {
|
|
2709
|
+
facts: (Array.isArray(result.facts) ? result.facts : []).map(validateFact).filter((f) => f !== null),
|
|
2710
|
+
ontology_updates: result.ontology_updates
|
|
2711
|
+
};
|
|
2438
2712
|
}),
|
|
2439
2713
|
chunkConcurrency
|
|
2440
2714
|
);
|
|
2441
2715
|
const seen = /* @__PURE__ */ new Set();
|
|
2442
|
-
const
|
|
2443
|
-
for (const
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2716
|
+
const orderedChunkFacts = [];
|
|
2717
|
+
for (const chunkResult of chunkResults) {
|
|
2718
|
+
const dedupedFacts = [];
|
|
2719
|
+
for (const fact of chunkResult.facts) {
|
|
2720
|
+
const normalizedTitle = normalizeTitleKey(fact.title);
|
|
2721
|
+
if (!seen.has(normalizedTitle)) {
|
|
2722
|
+
seen.add(normalizedTitle);
|
|
2723
|
+
dedupedFacts.push(fact);
|
|
2449
2724
|
}
|
|
2450
2725
|
}
|
|
2726
|
+
orderedChunkFacts.push({ facts: dedupedFacts, ontology_updates: chunkResult.ontology_updates });
|
|
2451
2727
|
}
|
|
2452
2728
|
const now = Date.now();
|
|
2453
2729
|
const insertedFacts = [];
|
|
@@ -2455,26 +2731,63 @@ var IngestionService = class {
|
|
|
2455
2731
|
await this.db.withTransactionAsync(async (tx) => {
|
|
2456
2732
|
deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
|
|
2457
2733
|
await this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, null);
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2734
|
+
const titleIndex = /* @__PURE__ */ new Map();
|
|
2735
|
+
const pendingEdges = [];
|
|
2736
|
+
const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx);
|
|
2737
|
+
for (const existing of existingFacts) {
|
|
2738
|
+
titleIndex.set(normalizeTitleKey(existing.title), {
|
|
2739
|
+
id: existing.id,
|
|
2740
|
+
okf_type: existing.okf_type ?? null
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
let ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
2744
|
+
let { mode, manifest } = ontologyState;
|
|
2745
|
+
for (const { facts, ontology_updates } of orderedChunkFacts) {
|
|
2746
|
+
if (mode === "emergent" && ontology_updates && this.ontologyService) {
|
|
2747
|
+
manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontology_updates, tx);
|
|
2748
|
+
ontologyState = await this.ontologyService.getEffectiveState(entityId, tx);
|
|
2749
|
+
mode = ontologyState.mode;
|
|
2750
|
+
}
|
|
2751
|
+
for (const fact of facts) {
|
|
2752
|
+
const ontologyFact = fact;
|
|
2753
|
+
const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
|
|
2754
|
+
const id = generateId("fact_");
|
|
2755
|
+
const wikiFact = {
|
|
2756
|
+
id,
|
|
2757
|
+
entity_id: entityId,
|
|
2758
|
+
title: fact.title,
|
|
2759
|
+
body: fact.body,
|
|
2760
|
+
tags: fact.tags,
|
|
2761
|
+
confidence: fact.confidence,
|
|
2762
|
+
source_type: "immutable_document",
|
|
2763
|
+
source_hash: sourceHash,
|
|
2764
|
+
source_ref: sourceRef,
|
|
2765
|
+
created_at: now,
|
|
2766
|
+
updated_at: now,
|
|
2767
|
+
last_accessed_at: null,
|
|
2768
|
+
access_count: 0,
|
|
2769
|
+
deleted_at: null,
|
|
2770
|
+
okf_type: normalized.okf_type
|
|
2771
|
+
};
|
|
2772
|
+
await this.entryRepo.upsert(wikiFact, tx);
|
|
2773
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
2774
|
+
titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
|
|
2775
|
+
if (normalized.edges.length > 0) {
|
|
2776
|
+
pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
for (const item of pendingEdges) {
|
|
2781
|
+
await this.ontologyService?.resolveAndPersistEdges(
|
|
2782
|
+
entityId,
|
|
2783
|
+
item.sourceId,
|
|
2784
|
+
item.sourceType,
|
|
2785
|
+
item.edges ?? [],
|
|
2786
|
+
manifest,
|
|
2787
|
+
titleIndex,
|
|
2788
|
+
tx,
|
|
2789
|
+
now
|
|
2790
|
+
);
|
|
2478
2791
|
}
|
|
2479
2792
|
});
|
|
2480
2793
|
await this.searchService.sync(entityId);
|
|
@@ -2501,7 +2814,7 @@ var IngestionService = class {
|
|
|
2501
2814
|
var FUZZY_THRESHOLD = 0.5;
|
|
2502
2815
|
var MIN_TOKENS_TO_QUALIFY = 3;
|
|
2503
2816
|
var MaintenanceService = class {
|
|
2504
|
-
constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService) {
|
|
2817
|
+
constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
2505
2818
|
this.db = db;
|
|
2506
2819
|
this.prefix = prefix;
|
|
2507
2820
|
this.options = options;
|
|
@@ -2512,6 +2825,7 @@ var MaintenanceService = class {
|
|
|
2512
2825
|
this.searchService = searchService;
|
|
2513
2826
|
this.jobManager = jobManager;
|
|
2514
2827
|
this.embeddingService = embeddingService;
|
|
2828
|
+
this.ontologyService = ontologyService;
|
|
2515
2829
|
this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
|
|
2516
2830
|
}
|
|
2517
2831
|
async runPrune(entityId, options) {
|
|
@@ -2734,21 +3048,36 @@ var MaintenanceService = class {
|
|
|
2734
3048
|
tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
|
|
2735
3049
|
};
|
|
2736
3050
|
});
|
|
3051
|
+
const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
|
|
2737
3052
|
const { systemPrompt, userPrompt } = this.promptService.buildLibrarianPrompt(
|
|
2738
3053
|
events.reverse(),
|
|
2739
3054
|
currentFacts,
|
|
2740
|
-
promptOverride
|
|
3055
|
+
promptOverride,
|
|
3056
|
+
ontologyContext
|
|
2741
3057
|
);
|
|
2742
3058
|
const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
|
|
2743
3059
|
const result = parseJsonResponse(responseText);
|
|
2744
3060
|
const facts = Array.isArray(result.facts) ? result.facts : [];
|
|
2745
3061
|
const tasks = Array.isArray(result.tasks) ? result.tasks : [];
|
|
3062
|
+
const ontologyUpdates = result.ontology_updates;
|
|
2746
3063
|
const validFacts = facts.map(validateFact).filter((f) => f !== null);
|
|
2747
3064
|
const validTasks = tasks.map(validateTask).filter((t) => t !== null);
|
|
2748
3065
|
const now = Date.now();
|
|
2749
3066
|
const insertedFacts = [];
|
|
2750
3067
|
await this.db.withTransactionAsync(async (tx) => {
|
|
3068
|
+
let { mode, manifest } = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
3069
|
+
if (mode === "emergent" && ontologyUpdates && this.ontologyService) {
|
|
3070
|
+
manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontologyUpdates, tx);
|
|
3071
|
+
}
|
|
3072
|
+
const titleIndex = /* @__PURE__ */ new Map();
|
|
3073
|
+
for (const existing of currentFactsRows) {
|
|
3074
|
+
titleIndex.set(normalizeTitleKey(existing.title), {
|
|
3075
|
+
id: existing.id,
|
|
3076
|
+
okf_type: existing.okf_type ?? null
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
2751
3079
|
const factsForDedupe = await this.entryRepo.findRecentByEntityId(entityId, 100, tx);
|
|
3080
|
+
const pendingEdges = [];
|
|
2752
3081
|
for (const fact of validFacts) {
|
|
2753
3082
|
const newTokens = titleTokens(fact.title);
|
|
2754
3083
|
let skip = false;
|
|
@@ -2765,6 +3094,8 @@ var MaintenanceService = class {
|
|
|
2765
3094
|
}
|
|
2766
3095
|
}
|
|
2767
3096
|
if (skip) continue;
|
|
3097
|
+
const ontologyFact = fact;
|
|
3098
|
+
const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
|
|
2768
3099
|
const id = generateId("fact_");
|
|
2769
3100
|
const factObj = {
|
|
2770
3101
|
id,
|
|
@@ -2780,11 +3111,28 @@ var MaintenanceService = class {
|
|
|
2780
3111
|
updated_at: now,
|
|
2781
3112
|
last_accessed_at: null,
|
|
2782
3113
|
access_count: 0,
|
|
2783
|
-
deleted_at: null
|
|
3114
|
+
deleted_at: null,
|
|
3115
|
+
okf_type: normalized.okf_type
|
|
2784
3116
|
};
|
|
2785
3117
|
await this.entryRepo.upsert(factObj, tx);
|
|
2786
3118
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
2787
3119
|
factsForDedupe.push(factObj);
|
|
3120
|
+
titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
|
|
3121
|
+
if (normalized.edges.length > 0) {
|
|
3122
|
+
pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
for (const item of pendingEdges) {
|
|
3126
|
+
await this.ontologyService?.resolveAndPersistEdges(
|
|
3127
|
+
entityId,
|
|
3128
|
+
item.sourceId,
|
|
3129
|
+
item.sourceType,
|
|
3130
|
+
item.edges ?? [],
|
|
3131
|
+
manifest,
|
|
3132
|
+
titleIndex,
|
|
3133
|
+
tx,
|
|
3134
|
+
now
|
|
3135
|
+
);
|
|
2788
3136
|
}
|
|
2789
3137
|
for (const task of validTasks) {
|
|
2790
3138
|
const id = generateId("task_");
|
|
@@ -4121,6 +4469,145 @@ var WriteService = class {
|
|
|
4121
4469
|
}
|
|
4122
4470
|
};
|
|
4123
4471
|
|
|
4472
|
+
// src/prompts/ontology.ts
|
|
4473
|
+
var FACT_ONTOLOGY_FIELDS = `
|
|
4474
|
+
Each fact may optionally include:
|
|
4475
|
+
- "okf_type": string \u2014 must be one of the node_types in the manifest
|
|
4476
|
+
- "edges": [{ "edge_type": string, "target_title": string }] \u2014 target_title must match another fact's title in this response or existing memory`;
|
|
4477
|
+
var EMERGENT_EXTRA = `
|
|
4478
|
+
You may also return "ontology_updates" to propose new types:
|
|
4479
|
+
"ontology_updates": {
|
|
4480
|
+
"node_types": [{ "type": "slug", "description": "..." }],
|
|
4481
|
+
"edge_types": [{ "type": "slug", "source_type": "...", "target_type": "...", "description": "..." }]
|
|
4482
|
+
}
|
|
4483
|
+
Only propose types not already in the manifest. Do not redefine existing types.`;
|
|
4484
|
+
function buildOntologyPromptAppendix(mode, manifestJson) {
|
|
4485
|
+
const strictRules = mode === "strict" ? "STRICT MODE: Use ONLY types defined in the manifest. If a fact does not fit any node_type, omit okf_type and edges entirely." : "EMERGENT MODE: Prefer manifest types. You may propose new types via ontology_updates when necessary.";
|
|
4486
|
+
const fallback = "If okf_type or an edge is invalid, omit them rather than inventing unlisted types.";
|
|
4487
|
+
const ontologyModeInstructions = [
|
|
4488
|
+
"## Ontology constraints",
|
|
4489
|
+
strictRules,
|
|
4490
|
+
fallback,
|
|
4491
|
+
FACT_ONTOLOGY_FIELDS,
|
|
4492
|
+
mode === "emergent" ? EMERGENT_EXTRA : "",
|
|
4493
|
+
"Manifest:",
|
|
4494
|
+
manifestJson
|
|
4495
|
+
].filter(Boolean).join("\n");
|
|
4496
|
+
return { ontologyManifest: manifestJson, ontologyModeInstructions };
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
// src/services/OntologyService.ts
|
|
4500
|
+
var OntologyService = class {
|
|
4501
|
+
constructor(metadataRepo, edgeRepo, ontologyConfig) {
|
|
4502
|
+
this.metadataRepo = metadataRepo;
|
|
4503
|
+
this.edgeRepo = edgeRepo;
|
|
4504
|
+
this.ontologyConfig = ontologyConfig;
|
|
4505
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
4506
|
+
}
|
|
4507
|
+
resolveMode(storedMode) {
|
|
4508
|
+
return storedMode ?? this.ontologyConfig?.mode ?? "off";
|
|
4509
|
+
}
|
|
4510
|
+
invalidateCache(entityId) {
|
|
4511
|
+
this.cache.delete(entityId);
|
|
4512
|
+
}
|
|
4513
|
+
async getEffectiveState(entityId, tx) {
|
|
4514
|
+
if (!tx) {
|
|
4515
|
+
const cached = this.cache.get(entityId);
|
|
4516
|
+
if (cached) return cached;
|
|
4517
|
+
}
|
|
4518
|
+
const row = await this.metadataRepo.getManifest(entityId, tx);
|
|
4519
|
+
if (row) {
|
|
4520
|
+
const state = { mode: this.resolveMode(row.mode), manifest: row.manifest };
|
|
4521
|
+
if (!tx) this.cache.set(entityId, state);
|
|
4522
|
+
return state;
|
|
4523
|
+
}
|
|
4524
|
+
const seed = this.ontologyConfig?.seedManifests?.[entityId];
|
|
4525
|
+
if (seed) {
|
|
4526
|
+
const state = {
|
|
4527
|
+
mode: this.resolveMode(seed.mode),
|
|
4528
|
+
manifest: seed.manifest
|
|
4529
|
+
};
|
|
4530
|
+
if (tx) {
|
|
4531
|
+
await this.metadataRepo.setManifest(entityId, state, tx);
|
|
4532
|
+
} else {
|
|
4533
|
+
this.cache.set(entityId, state);
|
|
4534
|
+
}
|
|
4535
|
+
return state;
|
|
4536
|
+
}
|
|
4537
|
+
return { mode: "off", manifest: emptyManifest() };
|
|
4538
|
+
}
|
|
4539
|
+
async buildPromptContext(entityId) {
|
|
4540
|
+
const { mode, manifest } = await this.getEffectiveState(entityId);
|
|
4541
|
+
if (mode === "off") return null;
|
|
4542
|
+
const manifestJson = JSON.stringify(manifest, null, 2);
|
|
4543
|
+
return buildOntologyPromptAppendix(mode, manifestJson);
|
|
4544
|
+
}
|
|
4545
|
+
async mergeEmergentUpdates(entityId, updates, tx) {
|
|
4546
|
+
const merged = await this.metadataRepo.mergeManifestUpdates(entityId, updates, tx);
|
|
4547
|
+
this.invalidateCache(entityId);
|
|
4548
|
+
return merged;
|
|
4549
|
+
}
|
|
4550
|
+
validateAndNormalizeFact(fact, manifest) {
|
|
4551
|
+
const rawType = typeof fact.okf_type === "string" ? fact.okf_type : "";
|
|
4552
|
+
const canonical = resolveNodeType(rawType, manifest);
|
|
4553
|
+
if (!canonical) return { okf_type: null, edges: [] };
|
|
4554
|
+
const edges = validateInlineEdges(canonical, null, fact.edges ?? [], manifest);
|
|
4555
|
+
return { okf_type: canonical, edges };
|
|
4556
|
+
}
|
|
4557
|
+
async resolveAndPersistEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, tx, now) {
|
|
4558
|
+
if (!sourceType || edges.length === 0) return;
|
|
4559
|
+
for (const edge of edges) {
|
|
4560
|
+
const def = resolveEdgeDefinition(edge.edge_type, manifest);
|
|
4561
|
+
if (!def || def.source_type.toLowerCase() !== sourceType.toLowerCase()) continue;
|
|
4562
|
+
const targetKey = normalizeTitleKey(edge.target_title);
|
|
4563
|
+
const target = titleIndex.get(targetKey);
|
|
4564
|
+
if (!target) continue;
|
|
4565
|
+
if (def.target_type.toLowerCase() !== (target.okf_type ?? "").toLowerCase()) continue;
|
|
4566
|
+
const wikiEdge = {
|
|
4567
|
+
id: generateId(),
|
|
4568
|
+
entity_id: entityId,
|
|
4569
|
+
source_id: sourceId,
|
|
4570
|
+
target_id: target.id,
|
|
4571
|
+
edge_type: def.type,
|
|
4572
|
+
created_at: now
|
|
4573
|
+
};
|
|
4574
|
+
await this.edgeRepo.addIgnoreDuplicate(wikiEdge, tx);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
};
|
|
4578
|
+
|
|
4579
|
+
// src/services/GraphTraversalService.ts
|
|
4580
|
+
var GraphTraversalService = class {
|
|
4581
|
+
constructor(edgeRepo, entryRepo, config) {
|
|
4582
|
+
this.edgeRepo = edgeRepo;
|
|
4583
|
+
this.entryRepo = entryRepo;
|
|
4584
|
+
this.config = config;
|
|
4585
|
+
}
|
|
4586
|
+
async traverseGraph(entityId, options) {
|
|
4587
|
+
const fallbackMaxNodes = 20;
|
|
4588
|
+
const rawConfigDefault = this.config.maxTraversalNodes ?? fallbackMaxNodes;
|
|
4589
|
+
const defaultMaxNodes = Number.isFinite(rawConfigDefault) && rawConfigDefault >= 1 ? Math.floor(rawConfigDefault) : fallbackMaxNodes;
|
|
4590
|
+
const rawMaxNodes = options.maxTraversalNodes ?? defaultMaxNodes;
|
|
4591
|
+
const maxNodes = Number.isFinite(rawMaxNodes) && rawMaxNodes >= 1 ? Math.floor(rawMaxNodes) : defaultMaxNodes;
|
|
4592
|
+
const opts = {
|
|
4593
|
+
maxDepth: Math.max(1, Math.min(options.maxDepth ?? 1, 3)),
|
|
4594
|
+
direction: options.direction ?? this.config.traversalDirection ?? "both",
|
|
4595
|
+
edgeTypes: options.edgeTypes,
|
|
4596
|
+
minConfidence: options.minTraversalConfidence ?? this.config.minTraversalConfidence ?? "tentative",
|
|
4597
|
+
excludeSourceTypes: options.excludeSourceTypes ?? this.config.excludeSourceTypes ?? [],
|
|
4598
|
+
maxNodes
|
|
4599
|
+
};
|
|
4600
|
+
const { nodeIds, edges } = await this.edgeRepo.getNeighborhood(entityId, options.sourceId, opts);
|
|
4601
|
+
if (nodeIds.length === 0) return { nodes: [], edges: [] };
|
|
4602
|
+
const nodes = await this.entryRepo.findByIds(nodeIds, [entityId]);
|
|
4603
|
+
const hydratedIds = new Set(nodes.map((node) => node.id));
|
|
4604
|
+
const filteredEdges = edges.filter(
|
|
4605
|
+
(edge) => hydratedIds.has(edge.source_id) && hydratedIds.has(edge.target_id)
|
|
4606
|
+
);
|
|
4607
|
+
return { nodes, edges: filteredEdges };
|
|
4608
|
+
}
|
|
4609
|
+
};
|
|
4610
|
+
|
|
4124
4611
|
// src/WikiMemory.ts
|
|
4125
4612
|
var TABLE_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,30}_$/;
|
|
4126
4613
|
var _testAccessNonTestEnvWarned;
|
|
@@ -4142,6 +4629,11 @@ var WikiMemory = class {
|
|
|
4142
4629
|
this.eventRepo = new EventRepository(db, this.prefix);
|
|
4143
4630
|
this.edgeRepo = new EdgeRepository(db, this.prefix);
|
|
4144
4631
|
this.metadataRepo = new MetadataRepository(db, this.prefix);
|
|
4632
|
+
this.ontologyService = new OntologyService(
|
|
4633
|
+
this.metadataRepo,
|
|
4634
|
+
this.edgeRepo,
|
|
4635
|
+
options.config?.ontology
|
|
4636
|
+
);
|
|
4145
4637
|
this.embeddingService = new EmbeddingService(this.db, this.options, this.entryRepo, this.metadataRepo);
|
|
4146
4638
|
this.searchService = new SearchService(this.entryRepo);
|
|
4147
4639
|
this.jobManager = new JobManager(this.prefix);
|
|
@@ -4154,7 +4646,8 @@ var WikiMemory = class {
|
|
|
4154
4646
|
this.searchService,
|
|
4155
4647
|
this.jobManager,
|
|
4156
4648
|
this.embeddingService,
|
|
4157
|
-
this.promptService
|
|
4649
|
+
this.promptService,
|
|
4650
|
+
this.ontologyService
|
|
4158
4651
|
);
|
|
4159
4652
|
this.maintenanceService = new MaintenanceService(
|
|
4160
4653
|
this.db,
|
|
@@ -4167,7 +4660,8 @@ var WikiMemory = class {
|
|
|
4167
4660
|
this.searchService,
|
|
4168
4661
|
this.jobManager,
|
|
4169
4662
|
this.embeddingService,
|
|
4170
|
-
this.promptService
|
|
4663
|
+
this.promptService,
|
|
4664
|
+
this.ontologyService
|
|
4171
4665
|
);
|
|
4172
4666
|
this.importExportService = new ImportExportService(
|
|
4173
4667
|
this.db,
|
|
@@ -4197,6 +4691,11 @@ var WikiMemory = class {
|
|
|
4197
4691
|
this.jobManager,
|
|
4198
4692
|
this.maintenanceService
|
|
4199
4693
|
);
|
|
4694
|
+
this.graphTraversalService = new GraphTraversalService(
|
|
4695
|
+
this.edgeRepo,
|
|
4696
|
+
this.entryRepo,
|
|
4697
|
+
this.options.config ?? {}
|
|
4698
|
+
);
|
|
4200
4699
|
}
|
|
4201
4700
|
/**
|
|
4202
4701
|
* Explicit escape hatch for test suites: typed access to composed services for mocks/spies.
|
|
@@ -4217,6 +4716,7 @@ var WikiMemory = class {
|
|
|
4217
4716
|
searchService: this.searchService,
|
|
4218
4717
|
writeService: this.writeService,
|
|
4219
4718
|
promptService: this.promptService,
|
|
4719
|
+
graphTraversalService: this.graphTraversalService,
|
|
4220
4720
|
entryRepo: this.entryRepo,
|
|
4221
4721
|
metadataRepo: this.metadataRepo,
|
|
4222
4722
|
jobManager: this.jobManager
|
|
@@ -4287,6 +4787,9 @@ var WikiMemory = class {
|
|
|
4287
4787
|
async read(entityId, query, options) {
|
|
4288
4788
|
return this.retrievalService.read(entityId, query, options);
|
|
4289
4789
|
}
|
|
4790
|
+
async traverseGraph(entityId, options) {
|
|
4791
|
+
return this.graphTraversalService.traverseGraph(entityId, options);
|
|
4792
|
+
}
|
|
4290
4793
|
async getMemoryBundle(entityId) {
|
|
4291
4794
|
return this.importExportService.getFullBundle(entityId, { maxEvents: 10 });
|
|
4292
4795
|
}
|
|
@@ -4362,6 +4865,34 @@ var WikiMemory = class {
|
|
|
4362
4865
|
async markOutboxEventsProcessed(eventIds) {
|
|
4363
4866
|
await this.outboxRepo.acknowledge(eventIds);
|
|
4364
4867
|
}
|
|
4868
|
+
/**
|
|
4869
|
+
* Returns the effective ontology mode and manifest for an entity.
|
|
4870
|
+
* Resolution order: persisted DB row → `WikiConfig.ontology.seedManifests[entityId]` → `null`.
|
|
4871
|
+
*/
|
|
4872
|
+
async getOntologyManifest(entityId) {
|
|
4873
|
+
const row = await this.metadataRepo.getManifest(entityId);
|
|
4874
|
+
if (row) return { mode: this.ontologyService.resolveMode(row.mode), manifest: row.manifest };
|
|
4875
|
+
const seed = this.options.config?.ontology?.seedManifests?.[entityId];
|
|
4876
|
+
if (seed) {
|
|
4877
|
+
return {
|
|
4878
|
+
mode: this.ontologyService.resolveMode(seed.mode),
|
|
4879
|
+
manifest: seed.manifest
|
|
4880
|
+
};
|
|
4881
|
+
}
|
|
4882
|
+
return null;
|
|
4883
|
+
}
|
|
4884
|
+
/**
|
|
4885
|
+
* Seeds or replaces an entity's ontology manifest and optional mode override.
|
|
4886
|
+
* Validates manifest invariants (unique type slugs, edge endpoints reference node types).
|
|
4887
|
+
* Invalidates the in-memory ontology cache for this entity.
|
|
4888
|
+
*/
|
|
4889
|
+
async setOntologyManifest(entityId, manifest, options) {
|
|
4890
|
+
const mode = options?.mode ?? this.ontologyService.resolveMode();
|
|
4891
|
+
await this.db.withTransactionAsync(
|
|
4892
|
+
(tx) => this.metadataRepo.setManifest(entityId, { mode, manifest }, tx)
|
|
4893
|
+
);
|
|
4894
|
+
this.ontologyService.invalidateCache(entityId);
|
|
4895
|
+
}
|
|
4365
4896
|
};
|
|
4366
4897
|
_testAccessNonTestEnvWarned = new WeakMap();
|
|
4367
4898
|
|
|
@@ -4486,6 +5017,38 @@ function formatContext(bundle, options) {
|
|
|
4486
5017
|
return lines.join("\n");
|
|
4487
5018
|
}
|
|
4488
5019
|
|
|
5020
|
+
// src/utils/formatGraphContext.ts
|
|
5021
|
+
function formatGraphContext(neighborhood) {
|
|
5022
|
+
const { nodes, edges } = neighborhood;
|
|
5023
|
+
if (nodes.length === 0) return "";
|
|
5024
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
5025
|
+
const nodeIndex = new Map(nodes.map((n, i) => [n.id, i]));
|
|
5026
|
+
const lines = [];
|
|
5027
|
+
for (const node of nodes) {
|
|
5028
|
+
lines.push(`[${node.okf_type ?? "fact"}] ${node.title} (ID: ${node.id})`);
|
|
5029
|
+
const outbound = edgesByDirection(edges, node.id, "source_id", nodeById, nodeIndex);
|
|
5030
|
+
const inbound = edgesByDirection(edges, node.id, "target_id", nodeById, nodeIndex);
|
|
5031
|
+
for (const { edge, other } of outbound) {
|
|
5032
|
+
lines.push(` -[${edge.edge_type}]-> [${other.okf_type ?? "fact"}] ${other.title}`);
|
|
5033
|
+
}
|
|
5034
|
+
for (const { edge, other } of inbound) {
|
|
5035
|
+
lines.push(` <-[${edge.edge_type}]- [${other.okf_type ?? "fact"}] ${other.title}`);
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
return lines.join("\n");
|
|
5039
|
+
}
|
|
5040
|
+
function edgesByDirection(edges, nodeId, endpoint, nodeById, nodeIndex) {
|
|
5041
|
+
const otherEndpoint = endpoint === "source_id" ? "target_id" : "source_id";
|
|
5042
|
+
return edges.filter((e) => e[endpoint] === nodeId).filter((e) => {
|
|
5043
|
+
const otherId = e[otherEndpoint];
|
|
5044
|
+
const selfIdx = nodeIndex.get(nodeId);
|
|
5045
|
+
const otherIdx = nodeIndex.get(otherId);
|
|
5046
|
+
return otherIdx !== void 0 && selfIdx < otherIdx;
|
|
5047
|
+
}).map((edge) => ({ edge, other: nodeById.get(edge[otherEndpoint]) })).sort(
|
|
5048
|
+
(a, b) => a.edge.edge_type.localeCompare(b.edge.edge_type) || a.other.title.localeCompare(b.other.title) || a.other.id.localeCompare(b.other.id) || a.edge.id.localeCompare(b.edge.id)
|
|
5049
|
+
);
|
|
5050
|
+
}
|
|
5051
|
+
|
|
4489
5052
|
// src/utils/sanitizeForFilename.ts
|
|
4490
5053
|
function shortHash(value) {
|
|
4491
5054
|
let h1 = 5381;
|
|
@@ -5002,6 +5565,7 @@ exports.WikiBusyError = WikiBusyError;
|
|
|
5002
5565
|
exports.WikiMemory = WikiMemory;
|
|
5003
5566
|
exports.createWiki = createWiki;
|
|
5004
5567
|
exports.formatContext = formatContext;
|
|
5568
|
+
exports.formatGraphContext = formatGraphContext;
|
|
5005
5569
|
exports.formatMemoryDump = formatMemoryDump;
|
|
5006
5570
|
exports.formatOkfBundle = formatOkfBundle;
|
|
5007
5571
|
exports.hydrateLibrarianPrompt = hydrateLibrarianPrompt;
|