@equationalapplications/core-llm-wiki 4.20.0 → 4.22.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, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, generateId, BaseRepository, emptyManifest, resolveNodeType, validateInlineEdges, resolveEdgeDefinition, normalizeTitleKey, WikiTransactionError, extractSqliteCode } from './chunk-HKB5FEJG.mjs';
2
- export { HOOK_TIMEOUT_MARKER, PromptService, PrunePartialFailureError, WikiBusyError, WikiTransactionError, configureRandomSource, parseEmbedding } from './chunk-HKB5FEJG.mjs';
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-LCAU6NYC.mjs';
2
+ export { HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiTransactionError, configureRandomSource, parseEmbedding, validateManifest } from './chunk-LCAU6NYC.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
@@ -22,7 +22,8 @@ async function setupDatabase(db, prefix) {
22
22
  deleted_at INTEGER,
23
23
  embedding TEXT,
24
24
  embedding_blob BLOB,
25
- okf_type TEXT
25
+ okf_type TEXT,
26
+ ontology_checked_at INTEGER
26
27
  );
27
28
 
28
29
  CREATE INDEX IF NOT EXISTS ${prefix}entries_entity_idx ON ${prefix}entries(entity_id);
@@ -248,6 +249,20 @@ var MIGRATIONS = [
248
249
  );
249
250
  `);
250
251
  }
252
+ },
253
+ {
254
+ version: 7,
255
+ description: "Add ontology_checked_at to entries for ontology backfill recheck cooldown",
256
+ run: async (db, prefix) => {
257
+ const cols = await db.getAllAsync(
258
+ `PRAGMA table_info(${prefix}entries)`
259
+ );
260
+ if (!cols.some((c) => c.name === "ontology_checked_at")) {
261
+ await db.execAsync(
262
+ `ALTER TABLE ${prefix}entries ADD COLUMN ontology_checked_at INTEGER`
263
+ );
264
+ }
265
+ }
251
266
  }
252
267
  ];
253
268
  for (let i = 1; i < MIGRATIONS.length; i++) {
@@ -996,6 +1011,79 @@ var EntryRepository = class extends BaseRepository {
996
1011
  );
997
1012
  return rows.map(mapRowToFact);
998
1013
  }
1014
+ /**
1015
+ * Live untyped facts eligible for ontology backfill, oldest first.
1016
+ * Skips facts checked within the recheck cooldown (ontology_checked_at > recheckCutoff).
1017
+ */
1018
+ async findUntypedByEntityId(entityId, limit, recheckCutoff, tx) {
1019
+ const executor = this.getExecutor(tx);
1020
+ const rows = await executor.getAllAsync(
1021
+ `SELECT * FROM ${this.prefix}entries
1022
+ WHERE entity_id = ? AND okf_type IS NULL AND deleted_at IS NULL
1023
+ AND (ontology_checked_at IS NULL OR ontology_checked_at <= ?)
1024
+ ORDER BY updated_at ASC LIMIT ?`,
1025
+ [entityId, recheckCutoff, limit]
1026
+ );
1027
+ return rows.map(mapRowToFact);
1028
+ }
1029
+ /** Counts live untyped facts: eligible (past cooldown) vs deferred (in cooldown). */
1030
+ async countUntypedByEntityId(entityId, recheckCutoff, tx) {
1031
+ const executor = this.getExecutor(tx);
1032
+ const row = await executor.getFirstAsync(
1033
+ `SELECT
1034
+ SUM(CASE WHEN ontology_checked_at IS NULL OR ontology_checked_at <= ? THEN 1 ELSE 0 END) AS eligible,
1035
+ SUM(CASE WHEN ontology_checked_at IS NOT NULL AND ontology_checked_at > ? THEN 1 ELSE 0 END) AS deferred
1036
+ FROM ${this.prefix}entries
1037
+ WHERE entity_id = ? AND okf_type IS NULL AND deleted_at IS NULL`,
1038
+ [recheckCutoff, recheckCutoff, entityId]
1039
+ );
1040
+ return { eligible: Number(row?.eligible ?? 0), deferred: Number(row?.deferred ?? 0) };
1041
+ }
1042
+ /**
1043
+ * Sets okf_type only when currently NULL — additive by construction: a
1044
+ * concurrently-typed fact is never overwritten. Returns changes for the caller
1045
+ * to distinguish applied vs skipped.
1046
+ */
1047
+ async updateOkfType(id, entityId, okfType, tx) {
1048
+ const result = await tx.runAsync(
1049
+ `UPDATE ${this.prefix}entries SET okf_type = ?
1050
+ WHERE id = ? AND entity_id = ? AND okf_type IS NULL AND deleted_at IS NULL`,
1051
+ [okfType, id, entityId]
1052
+ );
1053
+ return { changes: result.changes };
1054
+ }
1055
+ /**
1056
+ * Stamps the backfill recheck cooldown. NEVER touches updated_at — import
1057
+ * merge resolution is last-write-wins on updated_at and a bump here would
1058
+ * make an unchanged local fact beat a genuinely newer remote edit.
1059
+ */
1060
+ async markOntologyChecked(ids, entityId, now, tx) {
1061
+ if (ids.length === 0) return;
1062
+ for (let i = 0; i < ids.length; i += this.chunkSize) {
1063
+ const chunk = ids.slice(i, i + this.chunkSize);
1064
+ const placeholders = chunk.map(() => "?").join(",");
1065
+ await tx.runAsync(
1066
+ `UPDATE ${this.prefix}entries SET ontology_checked_at = ?
1067
+ WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
1068
+ [now, ...chunk, entityId]
1069
+ );
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Lightweight full-breadth title index (id, title, okf_type) over all live
1074
+ * typed facts. Used by ontology backfill edge resolution — a recent-N window
1075
+ * would miss an old fact's contemporaries. Untyped rows are excluded: they
1076
+ * can never pass the edge target-type check, and batch facts typed during
1077
+ * the run are re-added to the in-memory index by the caller.
1078
+ */
1079
+ async findTitleIndexByEntityId(entityId, tx) {
1080
+ const executor = this.getExecutor(tx);
1081
+ const rows = await executor.getAllAsync(
1082
+ `SELECT id, title, okf_type FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL AND okf_type IS NOT NULL`,
1083
+ [entityId]
1084
+ );
1085
+ return rows.map((r) => ({ id: r.id, title: r.title, okf_type: r.okf_type ?? null }));
1086
+ }
999
1087
  };
1000
1088
 
1001
1089
  // src/repositories/OutboxRepository.ts
@@ -1416,6 +1504,7 @@ var CONFIDENCE_RANK = {
1416
1504
  var EdgeRepository = class extends BaseRepository {
1417
1505
  /**
1418
1506
  * Insert an edge, silently skipping on primary-key or uniqueness conflicts.
1507
+ * Returns true when a row was inserted, false when skipped as a duplicate.
1419
1508
  * Throws when the insert was skipped due to an id collision with a different edge tuple.
1420
1509
  */
1421
1510
  async addIgnoreDuplicate(edge, tx) {
@@ -1425,17 +1514,18 @@ var EdgeRepository = class extends BaseRepository {
1425
1514
  VALUES (?, ?, ?, ?, ?, ?)`,
1426
1515
  [edge.id, edge.entity_id, edge.source_id, edge.target_id, edge.edge_type, edge.created_at]
1427
1516
  );
1428
- if (result.changes > 0) return;
1517
+ if (result.changes > 0) return true;
1429
1518
  const existing = await executor.getFirstAsync(
1430
1519
  `SELECT entity_id, source_id, target_id, edge_type FROM ${this.prefix}edges WHERE id = ?`,
1431
1520
  [edge.id]
1432
1521
  );
1433
- if (!existing) return;
1522
+ if (!existing) return false;
1434
1523
  if (String(existing.entity_id) !== edge.entity_id || String(existing.source_id) !== edge.source_id || String(existing.target_id) !== edge.target_id || String(existing.edge_type) !== edge.edge_type) {
1435
1524
  throw new Error(
1436
1525
  `Edge id collision: ${JSON.stringify(edge.id)} already exists with a different (entity_id, source_id, target_id, edge_type) tuple`
1437
1526
  );
1438
1527
  }
1528
+ return false;
1439
1529
  }
1440
1530
  async getByEntityId(entityId, tx) {
1441
1531
  const executor = this.getExecutor(tx);
@@ -1553,7 +1643,8 @@ function mapRowToEdge(row) {
1553
1643
  var FACT_ONTOLOGY_FIELDS = `
1554
1644
  Each fact may optionally include:
1555
1645
  - "okf_type": string \u2014 must be one of the node_types in the manifest
1556
- - "edges": [{ "edge_type": string, "target_title": string }] \u2014 target_title must match another fact's title in this response or existing memory`;
1646
+ - "edges": [{ "edge_type": string, "target_title": string }] \u2014 target_title must match another fact's title in this response or existing memory
1647
+ - An edge_type may appear in the manifest multiple times with different source_type/target_type; use the row whose types match your fact and target.`;
1557
1648
  var EMERGENT_EXTRA = `
1558
1649
  You may also return "ontology_updates" to propose new types:
1559
1650
  "ontology_updates": {
@@ -1635,14 +1726,18 @@ var OntologyService = class {
1635
1726
  return { okf_type: canonical, edges };
1636
1727
  }
1637
1728
  async resolveAndPersistEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, tx, now) {
1638
- if (!sourceType || edges.length === 0) return;
1729
+ if (!sourceType || edges.length === 0) return 0;
1730
+ let persisted = 0;
1639
1731
  for (const edge of edges) {
1640
- const def = resolveEdgeDefinition(edge.edge_type, manifest);
1641
- if (!def || def.source_type.toLowerCase() !== sourceType.toLowerCase()) continue;
1732
+ const candidates = resolveEdgeDefinitions(edge.edge_type, manifest).filter((d) => d.source_type.toLowerCase() === sourceType.toLowerCase());
1733
+ if (candidates.length === 0) continue;
1642
1734
  const targetKey = normalizeTitleKey(edge.target_title);
1643
1735
  const target = titleIndex.get(targetKey);
1644
1736
  if (!target) continue;
1645
- if (def.target_type.toLowerCase() !== (target.okf_type ?? "").toLowerCase()) continue;
1737
+ const def = candidates.find(
1738
+ (d) => d.target_type.toLowerCase() === (target.okf_type ?? "").toLowerCase()
1739
+ );
1740
+ if (!def) continue;
1646
1741
  const wikiEdge = {
1647
1742
  id: generateId(),
1648
1743
  entity_id: entityId,
@@ -1651,8 +1746,10 @@ var OntologyService = class {
1651
1746
  edge_type: def.type,
1652
1747
  created_at: now
1653
1748
  };
1654
- await this.edgeRepo.addIgnoreDuplicate(wikiEdge, tx);
1749
+ const inserted = await this.edgeRepo.addIgnoreDuplicate(wikiEdge, tx);
1750
+ if (inserted) persisted++;
1655
1751
  }
1752
+ return persisted;
1656
1753
  }
1657
1754
  };
1658
1755
 
@@ -1892,6 +1989,24 @@ var WikiMemory = class {
1892
1989
  async runHeal(entityId, options) {
1893
1990
  return this.maintenanceService.runHeal(entityId, options);
1894
1991
  }
1992
+ /**
1993
+ * Types already-persisted untyped facts (okf_type IS NULL) in place via one
1994
+ * librarian-style LLM call. Strictly additive: never creates, deletes, or
1995
+ * rewrites facts; never overwrites an existing okf_type. Free when there is
1996
+ * nothing to do (ontology off, or zero eligible untyped facts).
1997
+ *
1998
+ * Hosts own the trigger cadence (e.g. after each sync); loop `while
1999
+ * result.remaining > 0` for convergence. Throws WikiBusyError when another
2000
+ * backfill (or conflicting maintenance op) is running for the entity.
2001
+ *
2002
+ * @param options.promptOverride - Applies only to this call. For persistent
2003
+ * customization set `options.config.prompts.ontologyBackfillSystemPrompt`.
2004
+ * @param options.batchSize - Facts per run (default 25) for providers with
2005
+ * tighter context limits.
2006
+ */
2007
+ async runOntologyBackfill(entityId, options) {
2008
+ return this.maintenanceService.runOntologyBackfill(entityId, options);
2009
+ }
1895
2010
  async runReembed(entityId, opts) {
1896
2011
  return this.maintenanceService.runReembed(entityId, opts);
1897
2012
  }