@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/testing.js CHANGED
@@ -853,6 +853,15 @@ Return ONLY a valid JSON object matching this schema:
853
853
  "facts": [{ "title": "string (max 80 chars)", "body": "string (max 800 chars)", "tags": ["string"], "confidence": "certain|inferred|tentative" }]
854
854
  }
855
855
  Extract verbatim factual content. Do not return markdown, just raw JSON.`;
856
+ var ONTOLOGY_BACKFILL_SYSTEM_PROMPT = `You are a knowledge classification agent. You will receive existing memory facts that currently have no ontology type. For each input fact { "id", "title", "body", "tags" }, assign the best matching okf_type from the ontology manifest and optionally propose edges to related facts by title.
857
+ Return ONLY a valid JSON object matching this schema:
858
+ {
859
+ "classifications": [
860
+ { "id": "string (input fact id, copied verbatim)", "okf_type": "string (manifest node type slug)", "edges": [{ "edge_type": "string", "target_title": "string" }] }
861
+ ]
862
+ }
863
+ If no manifest type fits a fact, omit that fact from "classifications" entirely \u2014 do not guess.
864
+ Do not return markdown, just raw JSON.`;
856
865
 
857
866
  // src/services/PromptService.ts
858
867
  var PromptService = class {
@@ -942,6 +951,22 @@ ${JSON.stringify(recentEvents, null, 2)}
942
951
  The following document anchors are provided for contradiction detection only. Do not include them in \`downgraded\`, \`deleted\`, or \`newFacts\`.`
943
952
  };
944
953
  }
954
+ buildOntologyBackfillPrompt(facts, runtimeOverride, ontologyContext) {
955
+ const template = runtimeOverride ?? this.globalOverrides?.ontologyBackfillSystemPrompt ?? ONTOLOGY_BACKFILL_SYSTEM_PROMPT;
956
+ const hasFacts = /\{\{\s*facts\s*\}\}/.test(template);
957
+ if (hasFacts || this.hasOntologyPlaceholders(template)) {
958
+ return {
959
+ systemPrompt: this.buildSystemPrompt(template, { facts }, ontologyContext),
960
+ userPrompt: hasFacts ? "Please classify the facts." : `Facts:
961
+ ${JSON.stringify(facts, null, 2)}`
962
+ };
963
+ }
964
+ return {
965
+ systemPrompt: this.appendOntology(template, ontologyContext),
966
+ userPrompt: `Facts:
967
+ ${JSON.stringify(facts, null, 2)}`
968
+ };
969
+ }
945
970
  };
946
971
 
947
972
  // src/services/IngestionService.ts
@@ -1123,6 +1148,9 @@ function parseEmbedding(blob, text) {
1123
1148
  // src/services/MaintenanceService.ts
1124
1149
  var FUZZY_THRESHOLD = 0.5;
1125
1150
  var MIN_TOKENS_TO_QUALIFY = 3;
1151
+ var ONTOLOGY_BACKFILL_BATCH_SIZE = 25;
1152
+ var ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS = 4e4;
1153
+ var ONTOLOGY_BACKFILL_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
1126
1154
  var MaintenanceService = class {
1127
1155
  constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
1128
1156
  this.db = db;
@@ -1228,6 +1256,14 @@ var MaintenanceService = class {
1228
1256
  this.jobManager.releaseLock("heal", entityId);
1229
1257
  }
1230
1258
  }
1259
+ async runOntologyBackfill(entityId, options) {
1260
+ this.jobManager.acquireLock("ontologyBackfill", entityId);
1261
+ try {
1262
+ return await this.doRunOntologyBackfill(entityId, options);
1263
+ } finally {
1264
+ this.jobManager.releaseLock("ontologyBackfill", entityId);
1265
+ }
1266
+ }
1231
1267
  async runReembed(entityId, opts) {
1232
1268
  const embedFn = this.options.llmProvider.embed;
1233
1269
  if (!embedFn) return { embedded: 0, skipped: 0, failed: 0 };
@@ -1579,6 +1615,121 @@ var MaintenanceService = class {
1579
1615
  }
1580
1616
  this.searchService.evictCache(entityId);
1581
1617
  }
1618
+ /** Core ontology backfill pass (locks handled by {@link runOntologyBackfill}). Package-internal orchestration hook. */
1619
+ async doRunOntologyBackfill(entityId, options) {
1620
+ const batchSize = options?.batchSize ?? ONTOLOGY_BACKFILL_BATCH_SIZE;
1621
+ if (!Number.isInteger(batchSize) || batchSize < 1) {
1622
+ throw new Error("Invalid batchSize: must be an integer >= 1");
1623
+ }
1624
+ const now = Date.now();
1625
+ const recheckCutoff = now - ONTOLOGY_BACKFILL_RECHECK_MS;
1626
+ const zeroed = { scanned: 0, typed: 0, failedValidation: 0, edgesAdded: 0 };
1627
+ const ontologyService = this.ontologyService;
1628
+ if (!ontologyService) {
1629
+ return { ...zeroed, remaining: 0, deferred: 0 };
1630
+ }
1631
+ const { mode } = await ontologyService.getEffectiveState(entityId);
1632
+ if (mode === "off") {
1633
+ const counts2 = await this.entryRepo.countUntypedByEntityId(entityId, recheckCutoff);
1634
+ return { ...zeroed, remaining: 0, deferred: counts2.deferred };
1635
+ }
1636
+ const candidates = await this.entryRepo.findUntypedByEntityId(entityId, batchSize, recheckCutoff);
1637
+ if (candidates.length === 0) {
1638
+ const counts2 = await this.entryRepo.countUntypedByEntityId(entityId, recheckCutoff);
1639
+ return { ...zeroed, remaining: counts2.eligible, deferred: counts2.deferred };
1640
+ }
1641
+ const ontologyContext = await ontologyService.buildPromptContext(entityId);
1642
+ const toPromptShape = (f) => ({ id: f.id, title: f.title, body: f.body, tags: f.tags });
1643
+ const buildPrompt = (facts) => this.promptService.buildOntologyBackfillPrompt(
1644
+ facts.map(toPromptShape),
1645
+ options?.promptOverride,
1646
+ ontologyContext
1647
+ );
1648
+ const batch = [candidates[0]];
1649
+ let built = buildPrompt(batch);
1650
+ for (let i = 1; i < candidates.length; i++) {
1651
+ const next = buildPrompt([...batch, candidates[i]]);
1652
+ if (next.systemPrompt.length + next.userPrompt.length > ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS) break;
1653
+ batch.push(candidates[i]);
1654
+ built = next;
1655
+ }
1656
+ const { systemPrompt, userPrompt } = built;
1657
+ const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
1658
+ const parsed = parseJsonResponse(responseText);
1659
+ const classifications = Array.isArray(parsed.classifications) ? parsed.classifications : [];
1660
+ let typed = 0;
1661
+ let failedValidation = 0;
1662
+ let edgesAdded = 0;
1663
+ let abortedOntologyOff = false;
1664
+ await this.db.withTransactionAsync(async (tx) => {
1665
+ let { mode: txMode, manifest } = await ontologyService.getEffectiveState(entityId, tx);
1666
+ if (txMode === "off") {
1667
+ abortedOntologyOff = true;
1668
+ return;
1669
+ }
1670
+ if (txMode === "emergent" && parsed.ontology_updates) {
1671
+ manifest = await ontologyService.mergeEmergentUpdates(entityId, parsed.ontology_updates, tx);
1672
+ }
1673
+ const titleRows = await this.entryRepo.findTitleIndexByEntityId(entityId, tx);
1674
+ const titleIndex = /* @__PURE__ */ new Map();
1675
+ for (const row of titleRows) {
1676
+ titleIndex.set(normalizeTitleKey(row.title), { id: row.id, okf_type: row.okf_type });
1677
+ }
1678
+ const batchById = new Map(batch.map((f) => [f.id, f]));
1679
+ const applied = /* @__PURE__ */ new Set();
1680
+ const pendingEdges = [];
1681
+ for (const classification of classifications) {
1682
+ const fact = typeof classification.id === "string" ? batchById.get(classification.id) : void 0;
1683
+ if (!fact || applied.has(fact.id)) {
1684
+ failedValidation++;
1685
+ continue;
1686
+ }
1687
+ applied.add(fact.id);
1688
+ const normalized = ontologyService.validateAndNormalizeFact(
1689
+ {
1690
+ title: fact.title,
1691
+ body: fact.body,
1692
+ tags: fact.tags,
1693
+ confidence: fact.confidence,
1694
+ okf_type: classification.okf_type,
1695
+ edges: classification.edges
1696
+ },
1697
+ manifest
1698
+ );
1699
+ if (!normalized.okf_type) {
1700
+ failedValidation++;
1701
+ continue;
1702
+ }
1703
+ const result = await this.entryRepo.updateOkfType(fact.id, entityId, normalized.okf_type, tx);
1704
+ if (result.changes === 0) continue;
1705
+ typed++;
1706
+ titleIndex.set(normalizeTitleKey(fact.title), { id: fact.id, okf_type: normalized.okf_type });
1707
+ if (normalized.edges.length > 0) {
1708
+ pendingEdges.push({ sourceId: fact.id, sourceType: normalized.okf_type, edges: normalized.edges });
1709
+ }
1710
+ }
1711
+ for (const item of pendingEdges) {
1712
+ edgesAdded += await ontologyService.resolveAndPersistEdges(
1713
+ entityId,
1714
+ item.sourceId,
1715
+ item.sourceType,
1716
+ item.edges,
1717
+ manifest,
1718
+ titleIndex,
1719
+ tx,
1720
+ now
1721
+ );
1722
+ }
1723
+ await this.entryRepo.markOntologyChecked(batch.map((f) => f.id), entityId, now, tx);
1724
+ });
1725
+ if (abortedOntologyOff) {
1726
+ const counts2 = await this.entryRepo.countUntypedByEntityId(entityId, recheckCutoff);
1727
+ return { ...zeroed, remaining: 0, deferred: counts2.deferred };
1728
+ }
1729
+ this.searchService.evictCache(entityId);
1730
+ const counts = await this.entryRepo.countUntypedByEntityId(entityId, recheckCutoff);
1731
+ return { scanned: batch.length, typed, failedValidation, edgesAdded, remaining: counts.eligible, deferred: counts.deferred };
1732
+ }
1582
1733
  _validatePruneDuration(value, name) {
1583
1734
  if (value !== null && value !== void 0 && (typeof value !== "number" || !isFinite(value) || value < 0)) {
1584
1735
  throw new Error(`Invalid ${name}: must be a non-negative finite number or null`);
@@ -2360,7 +2511,8 @@ var JobManager = class {
2360
2511
  heal: (id) => this._healKey(id),
2361
2512
  reembed: (id) => this._reembedKey(id),
2362
2513
  import: (id) => this._importKey(id),
2363
- forget: (id) => this._forgetKey(id)
2514
+ forget: (id) => this._forgetKey(id),
2515
+ ontologyBackfill: (id) => this._ontologyBackfillKey(id)
2364
2516
  };
2365
2517
  }
2366
2518
  _pruneKey(entityId) {
@@ -2387,6 +2539,9 @@ var JobManager = class {
2387
2539
  _healKey(entityId) {
2388
2540
  return `${this.prefix}:${entityId}:heal`;
2389
2541
  }
2542
+ _ontologyBackfillKey(entityId) {
2543
+ return `${this.prefix}:${entityId}:ontologyBackfill`;
2544
+ }
2390
2545
  _isReembedActive(entityId) {
2391
2546
  return this.activeMaintenanceJobs.has(this._reembedKey(entityId)) || this.activeMaintenanceJobs.has(this._globalReembedKey());
2392
2547
  }
@@ -2437,14 +2592,16 @@ var JobManager = class {
2437
2592
  if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) blockingOperation = "prune";
2438
2593
  else if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) blockingOperation = "librarian";
2439
2594
  else if (this.activeMaintenanceJobs.has(this._healKey(entityId))) blockingOperation = "heal";
2595
+ else if (this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId))) blockingOperation = "ontologyBackfill";
2440
2596
  else if (this._isReembedActive(entityId)) blockingOperation = "reembed";
2441
2597
  else if (this._isIngestActiveFor(entityId)) blockingOperation = "ingest";
2442
2598
  else if (this._isImportActiveFor(entityId)) blockingOperation = "import";
2443
2599
  else if (this._isForgetActiveFor(entityId)) blockingOperation = "forget";
2444
2600
  break;
2445
2601
  case "librarian":
2446
- case "heal": {
2447
- const opKey = operation === "librarian" ? this._librarianKey(entityId) : this._healKey(entityId);
2602
+ case "heal":
2603
+ case "ontologyBackfill": {
2604
+ const opKey = this.lockKeyFns[operation](entityId);
2448
2605
  if (this.activeMaintenanceJobs.has(opKey)) blockingOperation = operation;
2449
2606
  else if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) blockingOperation = "prune";
2450
2607
  else if (this._isReembedActive(entityId)) blockingOperation = "reembed";
@@ -2458,6 +2615,7 @@ var JobManager = class {
2458
2615
  else if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) blockingOperation = "prune";
2459
2616
  else if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) blockingOperation = "librarian";
2460
2617
  else if (this.activeMaintenanceJobs.has(this._healKey(entityId))) blockingOperation = "heal";
2618
+ else if (this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId))) blockingOperation = "ontologyBackfill";
2461
2619
  else if (this._isIngestActiveFor(entityId)) blockingOperation = "ingest";
2462
2620
  else if (this._isImportActiveFor(entityId)) blockingOperation = "import";
2463
2621
  else if (this._isForgetActiveFor(entityId)) blockingOperation = "forget";
@@ -2468,6 +2626,7 @@ var JobManager = class {
2468
2626
  else if (this._isAnyMaintenanceActiveWithSuffix(":prune")) blockingOperation = "prune";
2469
2627
  else if (this._isAnyMaintenanceActiveWithSuffix(":librarian")) blockingOperation = "librarian";
2470
2628
  else if (this._isAnyMaintenanceActiveWithSuffix(":heal")) blockingOperation = "heal";
2629
+ else if (this._isAnyMaintenanceActiveWithSuffix(":ontologyBackfill")) blockingOperation = "ontologyBackfill";
2471
2630
  else if (this.activeIngestJobs.size > 0) blockingOperation = "ingest";
2472
2631
  else if (this._isAnyMaintenanceActiveWithSuffix(":import")) blockingOperation = "import";
2473
2632
  else if (this._isAnyMaintenanceActiveWithSuffix(":forget")) blockingOperation = "forget";
@@ -2478,6 +2637,7 @@ var JobManager = class {
2478
2637
  if (this.activeMaintenanceJobs.has(selfKey)) blockingOperation = operation;
2479
2638
  else if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) blockingOperation = "librarian";
2480
2639
  else if (this.activeMaintenanceJobs.has(this._healKey(entityId))) blockingOperation = "heal";
2640
+ else if (this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId))) blockingOperation = "ontologyBackfill";
2481
2641
  else if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) blockingOperation = "prune";
2482
2642
  else if (this._isReembedActive(entityId)) blockingOperation = "reembed";
2483
2643
  else if (this._isIngestActiveFor(entityId)) blockingOperation = "ingest";
@@ -2535,11 +2695,11 @@ var JobManager = class {
2535
2695
  if (operation !== "global_import" && this.activeMaintenanceJobs.has(this._globalImportKey())) return true;
2536
2696
  switch (operation) {
2537
2697
  case "librarian":
2538
- return this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2539
2698
  case "heal":
2540
- return this.activeMaintenanceJobs.has(this._healKey(entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2699
+ case "ontologyBackfill":
2700
+ return this.activeMaintenanceJobs.has(this.lockKeyFns[operation](entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2541
2701
  case "prune":
2542
- return this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._healKey(entityId)) || this._isReembedActive(entityId) || this._isIngestActiveFor(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2702
+ return this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._healKey(entityId)) || this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId)) || this._isReembedActive(entityId) || this._isIngestActiveFor(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
2543
2703
  default:
2544
2704
  return false;
2545
2705
  }
@@ -2564,6 +2724,7 @@ var JobManager = class {
2564
2724
  if (this.activeMaintenanceJobs.has(this._importKey(entityId))) throw new WikiBusyError("import", entityId);
2565
2725
  if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) throw new WikiBusyError("librarian", entityId);
2566
2726
  if (this.activeMaintenanceJobs.has(this._healKey(entityId))) throw new WikiBusyError("heal", entityId);
2727
+ if (this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId))) throw new WikiBusyError("ontologyBackfill", entityId);
2567
2728
  if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) throw new WikiBusyError("prune", entityId);
2568
2729
  if (this._isReembedActive(entityId)) throw new WikiBusyError("reembed", entityId);
2569
2730
  if (this._isIngestActiveFor(entityId)) throw new WikiBusyError("ingest", entityId);