@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/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { __privateAdd, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, generateId } from './chunk-J4GBC6CP.mjs';
2
- export { HOOK_TIMEOUT_MARKER, PromptService, PrunePartialFailureError, WikiBusyError, parseEmbedding } from './chunk-J4GBC6CP.mjs';
1
+ import { __privateAdd, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, generateId, validateManifest, emptyManifest, mergeOntologyUpdates, resolveNodeType, validateInlineEdges, resolveEdgeDefinition, normalizeTitleKey } from './chunk-M74FCWLB.mjs';
2
+ export { HOOK_TIMEOUT_MARKER, PromptService, PrunePartialFailureError, WikiBusyError, parseEmbedding } from './chunk-M74FCWLB.mjs';
3
3
  import { buildConceptDocument, buildLogMd, buildIndexMd, buildRootIndexMd, parseConcept, extractMarkdownLinks, parseLogMd } from '@equationalapplications/core-okf';
4
4
 
5
5
  // src/db/schema.ts
@@ -74,6 +74,13 @@ async function setupDatabase(db, prefix) {
74
74
  memory_checkpoint INTEGER NOT NULL DEFAULT 0
75
75
  );
76
76
 
77
+ CREATE TABLE IF NOT EXISTS ${prefix}entity_manifests (
78
+ entity_id TEXT PRIMARY KEY,
79
+ mode TEXT NOT NULL DEFAULT 'off',
80
+ manifest_json TEXT NOT NULL DEFAULT '{"node_types":[],"edge_types":[]}',
81
+ updated_at INTEGER NOT NULL
82
+ );
83
+
77
84
  CREATE TABLE IF NOT EXISTS ${prefix}meta (
78
85
  key TEXT PRIMARY KEY,
79
86
  value TEXT NOT NULL
@@ -183,6 +190,20 @@ var MIGRATIONS = [
183
190
  CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_idx ON ${prefix}edges (entity_id);
184
191
  `);
185
192
  }
193
+ },
194
+ {
195
+ version: 6,
196
+ description: "Add entity_manifests table for per-entity ontology state",
197
+ run: async (db, prefix) => {
198
+ await db.execAsync(`
199
+ CREATE TABLE IF NOT EXISTS ${prefix}entity_manifests (
200
+ entity_id TEXT PRIMARY KEY,
201
+ mode TEXT NOT NULL DEFAULT 'off',
202
+ manifest_json TEXT NOT NULL DEFAULT '{"node_types":[],"edge_types":[]}',
203
+ updated_at INTEGER NOT NULL
204
+ );
205
+ `);
206
+ }
186
207
  }
187
208
  ];
188
209
  for (let i = 1; i < MIGRATIONS.length; i++) {
@@ -306,8 +327,8 @@ var EntryRepository = class extends BaseRepository {
306
327
  `INSERT INTO ${this.prefix}entries (
307
328
  id, entity_id, title, body, tags, confidence, source_type,
308
329
  source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count,
309
- deleted_at, embedding_blob, embedding
310
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
330
+ deleted_at, embedding_blob, embedding, okf_type
331
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
311
332
  ON CONFLICT(id) DO UPDATE SET
312
333
  entity_id = excluded.entity_id,
313
334
  title = excluded.title,
@@ -322,7 +343,8 @@ var EntryRepository = class extends BaseRepository {
322
343
  access_count = excluded.access_count,
323
344
  deleted_at = excluded.deleted_at,
324
345
  embedding_blob = CASE WHEN excluded.embedding_blob IS NULL THEN embedding_blob ELSE excluded.embedding_blob END,
325
- embedding = NULL`,
346
+ embedding = NULL,
347
+ okf_type = excluded.okf_type`,
326
348
  [
327
349
  fact.id,
328
350
  fact.entity_id,
@@ -339,7 +361,8 @@ var EntryRepository = class extends BaseRepository {
339
361
  fact.access_count,
340
362
  fact.deleted_at ?? null,
341
363
  embeddingBlob ?? null,
342
- null
364
+ null,
365
+ fact.okf_type ?? null
343
366
  ]
344
367
  );
345
368
  await this.outbox.push({
@@ -1356,6 +1379,11 @@ var EventRepository = class extends BaseRepository {
1356
1379
  };
1357
1380
 
1358
1381
  // src/repositories/EdgeRepository.ts
1382
+ var CONFIDENCE_RANK = {
1383
+ tentative: 0,
1384
+ inferred: 1,
1385
+ certain: 2
1386
+ };
1359
1387
  var EdgeRepository = class extends BaseRepository {
1360
1388
  /**
1361
1389
  * Insert an edge, silently skipping on primary-key or uniqueness conflicts.
@@ -1386,21 +1414,111 @@ var EdgeRepository = class extends BaseRepository {
1386
1414
  `SELECT * FROM ${this.prefix}edges WHERE entity_id = ? ORDER BY created_at ASC`,
1387
1415
  [entityId]
1388
1416
  );
1389
- return rows.map((row) => ({
1390
- id: String(row.id),
1391
- entity_id: String(row.entity_id),
1392
- source_id: String(row.source_id),
1393
- target_id: String(row.target_id),
1394
- edge_type: String(row.edge_type),
1395
- created_at: Number(row.created_at)
1396
- }));
1417
+ return rows.map(mapRowToEdge);
1397
1418
  }
1398
1419
  /** Hard delete — edges have no soft-delete concept, only presence/absence. `tx` is REQUIRED. */
1399
1420
  async bulkDeleteByEntityId(entityId, tx) {
1400
1421
  const executor = this.getExecutor(tx);
1401
1422
  await executor.runAsync(`DELETE FROM ${this.prefix}edges WHERE entity_id = ?`, [entityId]);
1402
1423
  }
1424
+ /**
1425
+ * Multi-hop traversal from `sourceId` via SQLite `WITH RECURSIVE`. All filtering,
1426
+ * dead-ending, cycle-guarding, capping, and ordering happens in this one query.
1427
+ * The anchor is validated (exists, right entity, not soft-deleted) but never gated
1428
+ * by confidence/source_type — only nodes discovered beyond it are.
1429
+ */
1430
+ async getNeighborhood(entityId, sourceId, opts, tx) {
1431
+ const executor = this.getExecutor(tx);
1432
+ if (opts.edgeTypes && opts.edgeTypes.length === 0) {
1433
+ const anchor = await executor.getFirstAsync(
1434
+ `SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`,
1435
+ [sourceId, entityId]
1436
+ );
1437
+ return { nodeIds: anchor ? [anchor.id] : [], edges: [] };
1438
+ }
1439
+ const edgeTypesClause = opts.edgeTypes ? `e.edge_type IN (${opts.edgeTypes.map(() => "?").join(",")})` : "1=1";
1440
+ const excludeSourceTypesPlaceholders = opts.excludeSourceTypes.map(() => "?").join(",");
1441
+ const minConfidenceRank = CONFIDENCE_RANK[opts.minConfidence];
1442
+ const sql = `
1443
+ WITH RECURSIVE walk(node_id, depth, visited) AS (
1444
+ SELECT id, 0, ',' || id || ','
1445
+ FROM ${this.prefix}entries
1446
+ WHERE id = ? AND entity_id = ? AND deleted_at IS NULL
1447
+
1448
+ UNION
1449
+
1450
+ SELECT
1451
+ CASE WHEN e.source_id = w.node_id THEN e.target_id ELSE e.source_id END,
1452
+ w.depth + 1,
1453
+ w.visited || (CASE WHEN e.source_id = w.node_id THEN e.target_id ELSE e.source_id END) || ','
1454
+ FROM walk w
1455
+ JOIN ${this.prefix}edges e
1456
+ ON e.entity_id = ?
1457
+ AND (
1458
+ (? != 'inbound' AND e.source_id = w.node_id) OR
1459
+ (? != 'outbound' AND e.target_id = w.node_id)
1460
+ )
1461
+ AND (${edgeTypesClause})
1462
+ JOIN ${this.prefix}entries n
1463
+ ON n.id = (CASE WHEN e.source_id = w.node_id THEN e.target_id ELSE e.source_id END)
1464
+ AND n.entity_id = ?
1465
+ AND n.deleted_at IS NULL
1466
+ AND (
1467
+ CASE n.confidence
1468
+ WHEN 'tentative' THEN 0
1469
+ WHEN 'inferred' THEN 1
1470
+ WHEN 'certain' THEN 2
1471
+ ELSE -1
1472
+ END
1473
+ ) >= ?
1474
+ AND n.source_type NOT IN (${excludeSourceTypesPlaceholders})
1475
+ WHERE w.depth < ?
1476
+ AND instr(w.visited, ',' || (CASE WHEN e.source_id = w.node_id THEN e.target_id ELSE e.source_id END) || ',') = 0
1477
+ )
1478
+ SELECT node_id, MIN(depth) AS depth
1479
+ FROM walk
1480
+ GROUP BY node_id
1481
+ ORDER BY depth ASC, (SELECT updated_at FROM ${this.prefix}entries WHERE id = node_id) DESC
1482
+ LIMIT ?
1483
+ `;
1484
+ const params = [
1485
+ sourceId,
1486
+ entityId,
1487
+ entityId,
1488
+ opts.direction,
1489
+ opts.direction,
1490
+ ...opts.edgeTypes ?? [],
1491
+ entityId,
1492
+ minConfidenceRank,
1493
+ ...opts.excludeSourceTypes,
1494
+ opts.maxDepth,
1495
+ opts.maxNodes
1496
+ ];
1497
+ const rows = await executor.getAllAsync(sql, params);
1498
+ const nodeIds = rows.map((r) => r.node_id);
1499
+ if (nodeIds.length === 0) return { nodeIds: [], edges: [] };
1500
+ const valueRows = nodeIds.map(() => "(?)").join(", ");
1501
+ const edgeRows = await executor.getAllAsync(
1502
+ `WITH neighborhood(node_id) AS (VALUES ${valueRows})
1503
+ SELECT e.* FROM ${this.prefix}edges e
1504
+ JOIN neighborhood ns ON e.source_id = ns.node_id
1505
+ JOIN neighborhood nt ON e.target_id = nt.node_id
1506
+ WHERE e.entity_id = ?`,
1507
+ [...nodeIds, entityId]
1508
+ );
1509
+ return { nodeIds, edges: edgeRows.map(mapRowToEdge) };
1510
+ }
1403
1511
  };
1512
+ function mapRowToEdge(row) {
1513
+ return {
1514
+ id: String(row.id),
1515
+ entity_id: String(row.entity_id),
1516
+ source_id: String(row.source_id),
1517
+ target_id: String(row.target_id),
1518
+ edge_type: String(row.edge_type),
1519
+ created_at: Number(row.created_at)
1520
+ };
1521
+ }
1404
1522
 
1405
1523
  // src/repositories/MetadataRepository.ts
1406
1524
  var MetadataRepository = class extends BaseRepository {
@@ -1500,6 +1618,183 @@ var MetadataRepository = class extends BaseRepository {
1500
1618
  );
1501
1619
  return rows.map((r) => r.entity_id);
1502
1620
  }
1621
+ async getManifest(entityId, tx) {
1622
+ const executor = this.getExecutor(tx);
1623
+ const row = await executor.getFirstAsync(`SELECT mode, manifest_json FROM ${this.prefix}entity_manifests WHERE entity_id = ?`, [entityId]);
1624
+ if (!row) return null;
1625
+ if (row.mode !== "off" && row.mode !== "strict" && row.mode !== "emergent") {
1626
+ throw new Error(`Invalid ontology mode for entity ${entityId}: ${JSON.stringify(row.mode)}`);
1627
+ }
1628
+ let manifest;
1629
+ try {
1630
+ manifest = JSON.parse(row.manifest_json);
1631
+ } catch (error) {
1632
+ throw new Error(`Invalid manifest_json for entity ${entityId}: ${error.message}`);
1633
+ }
1634
+ validateManifest(manifest);
1635
+ return {
1636
+ mode: row.mode,
1637
+ manifest
1638
+ };
1639
+ }
1640
+ async setManifest(entityId, data, tx) {
1641
+ validateManifest(data.manifest);
1642
+ const executor = this.getExecutor(tx);
1643
+ await executor.runAsync(
1644
+ `INSERT INTO ${this.prefix}entity_manifests (entity_id, mode, manifest_json, updated_at)
1645
+ VALUES (?, ?, ?, ?)
1646
+ ON CONFLICT(entity_id) DO UPDATE SET mode = excluded.mode, manifest_json = excluded.manifest_json, updated_at = excluded.updated_at`,
1647
+ [entityId, data.mode, JSON.stringify(data.manifest), Date.now()]
1648
+ );
1649
+ }
1650
+ async mergeManifestUpdates(entityId, updates, tx) {
1651
+ const current = await this.getManifest(entityId, tx) ?? {
1652
+ mode: "emergent",
1653
+ manifest: emptyManifest()
1654
+ };
1655
+ const merged = mergeOntologyUpdates(current.manifest, updates);
1656
+ await this.setManifest(entityId, { mode: current.mode, manifest: merged }, tx);
1657
+ return merged;
1658
+ }
1659
+ };
1660
+
1661
+ // src/prompts/ontology.ts
1662
+ var FACT_ONTOLOGY_FIELDS = `
1663
+ Each fact may optionally include:
1664
+ - "okf_type": string \u2014 must be one of the node_types in the manifest
1665
+ - "edges": [{ "edge_type": string, "target_title": string }] \u2014 target_title must match another fact's title in this response or existing memory`;
1666
+ var EMERGENT_EXTRA = `
1667
+ You may also return "ontology_updates" to propose new types:
1668
+ "ontology_updates": {
1669
+ "node_types": [{ "type": "slug", "description": "..." }],
1670
+ "edge_types": [{ "type": "slug", "source_type": "...", "target_type": "...", "description": "..." }]
1671
+ }
1672
+ Only propose types not already in the manifest. Do not redefine existing types.`;
1673
+ function buildOntologyPromptAppendix(mode, manifestJson) {
1674
+ 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.";
1675
+ const fallback = "If okf_type or an edge is invalid, omit them rather than inventing unlisted types.";
1676
+ const ontologyModeInstructions = [
1677
+ "## Ontology constraints",
1678
+ strictRules,
1679
+ fallback,
1680
+ FACT_ONTOLOGY_FIELDS,
1681
+ mode === "emergent" ? EMERGENT_EXTRA : "",
1682
+ "Manifest:",
1683
+ manifestJson
1684
+ ].filter(Boolean).join("\n");
1685
+ return { ontologyManifest: manifestJson, ontologyModeInstructions };
1686
+ }
1687
+
1688
+ // src/services/OntologyService.ts
1689
+ var OntologyService = class {
1690
+ constructor(metadataRepo, edgeRepo, ontologyConfig) {
1691
+ this.metadataRepo = metadataRepo;
1692
+ this.edgeRepo = edgeRepo;
1693
+ this.ontologyConfig = ontologyConfig;
1694
+ this.cache = /* @__PURE__ */ new Map();
1695
+ }
1696
+ resolveMode(storedMode) {
1697
+ return storedMode ?? this.ontologyConfig?.mode ?? "off";
1698
+ }
1699
+ invalidateCache(entityId) {
1700
+ this.cache.delete(entityId);
1701
+ }
1702
+ async getEffectiveState(entityId, tx) {
1703
+ if (!tx) {
1704
+ const cached = this.cache.get(entityId);
1705
+ if (cached) return cached;
1706
+ }
1707
+ const row = await this.metadataRepo.getManifest(entityId, tx);
1708
+ if (row) {
1709
+ const state = { mode: this.resolveMode(row.mode), manifest: row.manifest };
1710
+ if (!tx) this.cache.set(entityId, state);
1711
+ return state;
1712
+ }
1713
+ const seed = this.ontologyConfig?.seedManifests?.[entityId];
1714
+ if (seed) {
1715
+ const state = {
1716
+ mode: this.resolveMode(seed.mode),
1717
+ manifest: seed.manifest
1718
+ };
1719
+ if (tx) {
1720
+ await this.metadataRepo.setManifest(entityId, state, tx);
1721
+ } else {
1722
+ this.cache.set(entityId, state);
1723
+ }
1724
+ return state;
1725
+ }
1726
+ return { mode: "off", manifest: emptyManifest() };
1727
+ }
1728
+ async buildPromptContext(entityId) {
1729
+ const { mode, manifest } = await this.getEffectiveState(entityId);
1730
+ if (mode === "off") return null;
1731
+ const manifestJson = JSON.stringify(manifest, null, 2);
1732
+ return buildOntologyPromptAppendix(mode, manifestJson);
1733
+ }
1734
+ async mergeEmergentUpdates(entityId, updates, tx) {
1735
+ const merged = await this.metadataRepo.mergeManifestUpdates(entityId, updates, tx);
1736
+ this.invalidateCache(entityId);
1737
+ return merged;
1738
+ }
1739
+ validateAndNormalizeFact(fact, manifest) {
1740
+ const rawType = typeof fact.okf_type === "string" ? fact.okf_type : "";
1741
+ const canonical = resolveNodeType(rawType, manifest);
1742
+ if (!canonical) return { okf_type: null, edges: [] };
1743
+ const edges = validateInlineEdges(canonical, null, fact.edges ?? [], manifest);
1744
+ return { okf_type: canonical, edges };
1745
+ }
1746
+ async resolveAndPersistEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, tx, now) {
1747
+ if (!sourceType || edges.length === 0) return;
1748
+ for (const edge of edges) {
1749
+ const def = resolveEdgeDefinition(edge.edge_type, manifest);
1750
+ if (!def || def.source_type.toLowerCase() !== sourceType.toLowerCase()) continue;
1751
+ const targetKey = normalizeTitleKey(edge.target_title);
1752
+ const target = titleIndex.get(targetKey);
1753
+ if (!target) continue;
1754
+ if (def.target_type.toLowerCase() !== (target.okf_type ?? "").toLowerCase()) continue;
1755
+ const wikiEdge = {
1756
+ id: generateId(),
1757
+ entity_id: entityId,
1758
+ source_id: sourceId,
1759
+ target_id: target.id,
1760
+ edge_type: def.type,
1761
+ created_at: now
1762
+ };
1763
+ await this.edgeRepo.addIgnoreDuplicate(wikiEdge, tx);
1764
+ }
1765
+ }
1766
+ };
1767
+
1768
+ // src/services/GraphTraversalService.ts
1769
+ var GraphTraversalService = class {
1770
+ constructor(edgeRepo, entryRepo, config) {
1771
+ this.edgeRepo = edgeRepo;
1772
+ this.entryRepo = entryRepo;
1773
+ this.config = config;
1774
+ }
1775
+ async traverseGraph(entityId, options) {
1776
+ const fallbackMaxNodes = 20;
1777
+ const rawConfigDefault = this.config.maxTraversalNodes ?? fallbackMaxNodes;
1778
+ const defaultMaxNodes = Number.isFinite(rawConfigDefault) && rawConfigDefault >= 1 ? Math.floor(rawConfigDefault) : fallbackMaxNodes;
1779
+ const rawMaxNodes = options.maxTraversalNodes ?? defaultMaxNodes;
1780
+ const maxNodes = Number.isFinite(rawMaxNodes) && rawMaxNodes >= 1 ? Math.floor(rawMaxNodes) : defaultMaxNodes;
1781
+ const opts = {
1782
+ maxDepth: Math.max(1, Math.min(options.maxDepth ?? 1, 3)),
1783
+ direction: options.direction ?? this.config.traversalDirection ?? "both",
1784
+ edgeTypes: options.edgeTypes,
1785
+ minConfidence: options.minTraversalConfidence ?? this.config.minTraversalConfidence ?? "tentative",
1786
+ excludeSourceTypes: options.excludeSourceTypes ?? this.config.excludeSourceTypes ?? [],
1787
+ maxNodes
1788
+ };
1789
+ const { nodeIds, edges } = await this.edgeRepo.getNeighborhood(entityId, options.sourceId, opts);
1790
+ if (nodeIds.length === 0) return { nodes: [], edges: [] };
1791
+ const nodes = await this.entryRepo.findByIds(nodeIds, [entityId]);
1792
+ const hydratedIds = new Set(nodes.map((node) => node.id));
1793
+ const filteredEdges = edges.filter(
1794
+ (edge) => hydratedIds.has(edge.source_id) && hydratedIds.has(edge.target_id)
1795
+ );
1796
+ return { nodes, edges: filteredEdges };
1797
+ }
1503
1798
  };
1504
1799
 
1505
1800
  // src/WikiMemory.ts
@@ -1523,6 +1818,11 @@ var WikiMemory = class {
1523
1818
  this.eventRepo = new EventRepository(db, this.prefix);
1524
1819
  this.edgeRepo = new EdgeRepository(db, this.prefix);
1525
1820
  this.metadataRepo = new MetadataRepository(db, this.prefix);
1821
+ this.ontologyService = new OntologyService(
1822
+ this.metadataRepo,
1823
+ this.edgeRepo,
1824
+ options.config?.ontology
1825
+ );
1526
1826
  this.embeddingService = new EmbeddingService(this.db, this.options, this.entryRepo, this.metadataRepo);
1527
1827
  this.searchService = new SearchService(this.entryRepo);
1528
1828
  this.jobManager = new JobManager(this.prefix);
@@ -1535,7 +1835,8 @@ var WikiMemory = class {
1535
1835
  this.searchService,
1536
1836
  this.jobManager,
1537
1837
  this.embeddingService,
1538
- this.promptService
1838
+ this.promptService,
1839
+ this.ontologyService
1539
1840
  );
1540
1841
  this.maintenanceService = new MaintenanceService(
1541
1842
  this.db,
@@ -1548,7 +1849,8 @@ var WikiMemory = class {
1548
1849
  this.searchService,
1549
1850
  this.jobManager,
1550
1851
  this.embeddingService,
1551
- this.promptService
1852
+ this.promptService,
1853
+ this.ontologyService
1552
1854
  );
1553
1855
  this.importExportService = new ImportExportService(
1554
1856
  this.db,
@@ -1578,6 +1880,11 @@ var WikiMemory = class {
1578
1880
  this.jobManager,
1579
1881
  this.maintenanceService
1580
1882
  );
1883
+ this.graphTraversalService = new GraphTraversalService(
1884
+ this.edgeRepo,
1885
+ this.entryRepo,
1886
+ this.options.config ?? {}
1887
+ );
1581
1888
  }
1582
1889
  /**
1583
1890
  * Explicit escape hatch for test suites: typed access to composed services for mocks/spies.
@@ -1598,6 +1905,7 @@ var WikiMemory = class {
1598
1905
  searchService: this.searchService,
1599
1906
  writeService: this.writeService,
1600
1907
  promptService: this.promptService,
1908
+ graphTraversalService: this.graphTraversalService,
1601
1909
  entryRepo: this.entryRepo,
1602
1910
  metadataRepo: this.metadataRepo,
1603
1911
  jobManager: this.jobManager
@@ -1668,6 +1976,9 @@ var WikiMemory = class {
1668
1976
  async read(entityId, query, options) {
1669
1977
  return this.retrievalService.read(entityId, query, options);
1670
1978
  }
1979
+ async traverseGraph(entityId, options) {
1980
+ return this.graphTraversalService.traverseGraph(entityId, options);
1981
+ }
1671
1982
  async getMemoryBundle(entityId) {
1672
1983
  return this.importExportService.getFullBundle(entityId, { maxEvents: 10 });
1673
1984
  }
@@ -1743,6 +2054,34 @@ var WikiMemory = class {
1743
2054
  async markOutboxEventsProcessed(eventIds) {
1744
2055
  await this.outboxRepo.acknowledge(eventIds);
1745
2056
  }
2057
+ /**
2058
+ * Returns the effective ontology mode and manifest for an entity.
2059
+ * Resolution order: persisted DB row → `WikiConfig.ontology.seedManifests[entityId]` → `null`.
2060
+ */
2061
+ async getOntologyManifest(entityId) {
2062
+ const row = await this.metadataRepo.getManifest(entityId);
2063
+ if (row) return { mode: this.ontologyService.resolveMode(row.mode), manifest: row.manifest };
2064
+ const seed = this.options.config?.ontology?.seedManifests?.[entityId];
2065
+ if (seed) {
2066
+ return {
2067
+ mode: this.ontologyService.resolveMode(seed.mode),
2068
+ manifest: seed.manifest
2069
+ };
2070
+ }
2071
+ return null;
2072
+ }
2073
+ /**
2074
+ * Seeds or replaces an entity's ontology manifest and optional mode override.
2075
+ * Validates manifest invariants (unique type slugs, edge endpoints reference node types).
2076
+ * Invalidates the in-memory ontology cache for this entity.
2077
+ */
2078
+ async setOntologyManifest(entityId, manifest, options) {
2079
+ const mode = options?.mode ?? this.ontologyService.resolveMode();
2080
+ await this.db.withTransactionAsync(
2081
+ (tx) => this.metadataRepo.setManifest(entityId, { mode, manifest }, tx)
2082
+ );
2083
+ this.ontologyService.invalidateCache(entityId);
2084
+ }
1746
2085
  };
1747
2086
  _testAccessNonTestEnvWarned = new WeakMap();
1748
2087
 
@@ -1867,6 +2206,38 @@ function formatContext(bundle, options) {
1867
2206
  return lines.join("\n");
1868
2207
  }
1869
2208
 
2209
+ // src/utils/formatGraphContext.ts
2210
+ function formatGraphContext(neighborhood) {
2211
+ const { nodes, edges } = neighborhood;
2212
+ if (nodes.length === 0) return "";
2213
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
2214
+ const nodeIndex = new Map(nodes.map((n, i) => [n.id, i]));
2215
+ const lines = [];
2216
+ for (const node of nodes) {
2217
+ lines.push(`[${node.okf_type ?? "fact"}] ${node.title} (ID: ${node.id})`);
2218
+ const outbound = edgesByDirection(edges, node.id, "source_id", nodeById, nodeIndex);
2219
+ const inbound = edgesByDirection(edges, node.id, "target_id", nodeById, nodeIndex);
2220
+ for (const { edge, other } of outbound) {
2221
+ lines.push(` -[${edge.edge_type}]-> [${other.okf_type ?? "fact"}] ${other.title}`);
2222
+ }
2223
+ for (const { edge, other } of inbound) {
2224
+ lines.push(` <-[${edge.edge_type}]- [${other.okf_type ?? "fact"}] ${other.title}`);
2225
+ }
2226
+ }
2227
+ return lines.join("\n");
2228
+ }
2229
+ function edgesByDirection(edges, nodeId, endpoint, nodeById, nodeIndex) {
2230
+ const otherEndpoint = endpoint === "source_id" ? "target_id" : "source_id";
2231
+ return edges.filter((e) => e[endpoint] === nodeId).filter((e) => {
2232
+ const otherId = e[otherEndpoint];
2233
+ const selfIdx = nodeIndex.get(nodeId);
2234
+ const otherIdx = nodeIndex.get(otherId);
2235
+ return otherIdx !== void 0 && selfIdx < otherIdx;
2236
+ }).map((edge) => ({ edge, other: nodeById.get(edge[otherEndpoint]) })).sort(
2237
+ (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)
2238
+ );
2239
+ }
2240
+
1870
2241
  // src/utils/sanitizeForFilename.ts
1871
2242
  function shortHash(value) {
1872
2243
  let h1 = 5381;
@@ -2375,6 +2746,6 @@ function createWiki(db, options) {
2375
2746
  return new WikiMemory(db, options);
2376
2747
  }
2377
2748
 
2378
- export { DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT, WikiMemory, createWiki, formatContext, formatMemoryDump, formatOkfBundle, hydrateLibrarianPrompt, mapLibrarianOptionsToReadOptions, parseOkfBundle, validateLibrarianPromptTemplate };
2749
+ export { DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT, WikiMemory, createWiki, formatContext, formatGraphContext, formatMemoryDump, formatOkfBundle, hydrateLibrarianPrompt, mapLibrarianOptionsToReadOptions, parseOkfBundle, validateLibrarianPromptTemplate };
2379
2750
  //# sourceMappingURL=index.mjs.map
2380
2751
  //# sourceMappingURL=index.mjs.map