@equationalapplications/core-llm-wiki 7.6.0 → 7.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, resolveGrounding, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, DiagnosticBuffer, validateManifest, WikiDraftNotFound, generateId, BaseRepository, WikiGraphNodeOwnershipConflict, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, typeSatisfies, normalizeTitleKey, WikiTransactionError, extractSqliteCode } from './chunk-YBWE26XR.mjs';
2
- export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-YBWE26XR.mjs';
1
+ import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, resolveGrounding, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, DiagnosticBuffer, validateManifest, WikiDraftNotFound, generateId, BaseRepository, WikiGraphNodeOwnershipConflict, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, typeSatisfies, normalizeTitleKey, edgeTripleAllowed, WikiTransactionError, extractSqliteCode } from './chunk-NWODQRSM.mjs';
2
+ export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-NWODQRSM.mjs';
3
3
  import { appendRelatedSection, buildConceptDocument, buildLogMd, buildEntityIndexMd, buildRootIndexMd, isAllowedOkfPath, parseRootIndexMd, parseEntityIndexMd, parseConcept, splitRelatedSection, extractMarkdownLinks, parseLogMd, parseEventIdComment, latestVerified, formatSourcesJson, appendEventIdComment, parseVerifiedFlexible, parseCitationsList, deriveTrustTier, isStaleAfter } from '@equationalapplications/core-okf';
4
4
 
5
5
  // src/db/schema.ts
@@ -94,7 +94,7 @@ async function setupDatabase(db, prefix) {
94
94
  UNIQUE(entity_id, source_id, target_id, edge_type)
95
95
  );
96
96
 
97
- CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_idx ON ${prefix}edges(entity_id);
97
+ CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_id_idx ON ${prefix}edges(entity_id, id);
98
98
 
99
99
  CREATE TABLE IF NOT EXISTS ${prefix}events (
100
100
  id TEXT PRIMARY KEY,
@@ -439,6 +439,16 @@ var MIGRATIONS = [
439
439
  if (!existing.has(name)) await db.execAsync(sql);
440
440
  }
441
441
  }
442
+ },
443
+ {
444
+ version: 12,
445
+ description: "Replace edges_entity_idx with composite edges(entity_id, id) for keyset paging",
446
+ run: async (db, prefix) => {
447
+ await db.execAsync(`
448
+ CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_id_idx ON ${prefix}edges(entity_id, id);
449
+ DROP INDEX IF EXISTS ${prefix}edges_entity_idx;
450
+ `);
451
+ }
442
452
  }
443
453
  ];
444
454
  for (let i = 1; i < MIGRATIONS.length; i++) {
@@ -1396,6 +1406,35 @@ var EntryRepository = class extends BaseRepository {
1396
1406
  }
1397
1407
  return out;
1398
1408
  }
1409
+ /**
1410
+ * Per source ref, the hash of the latest live row (same ordering as
1411
+ * {@link findLatestSourceHashes}) and whether ANY live row carries a hash.
1412
+ * Refs with no live row are absent. Chunked under the bind-variable limit.
1413
+ */
1414
+ async findSourceStates(entityId, sourceRefs, tx) {
1415
+ const out = /* @__PURE__ */ new Map();
1416
+ const deduped = Array.from(new Set(sourceRefs));
1417
+ if (deduped.length === 0) return out;
1418
+ const executor = this.getExecutor(tx);
1419
+ const chunkLimit = Math.max(1, this.chunkSize - 1);
1420
+ for (let i = 0; i < deduped.length; i += chunkLimit) {
1421
+ const chunk = deduped.slice(i, i + chunkLimit);
1422
+ const placeholders = chunk.map(() => "?").join(",");
1423
+ const rows = await executor.getAllAsync(
1424
+ `WITH ranked AS (
1425
+ SELECT source_ref, source_hash,
1426
+ ROW_NUMBER() OVER (PARTITION BY source_ref ORDER BY updated_at DESC, id ASC) AS rn,
1427
+ MAX(CASE WHEN source_hash IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY source_ref) AS any_hashed
1428
+ FROM ${this.prefix}entries
1429
+ WHERE entity_id = ? AND source_ref IN (${placeholders}) AND deleted_at IS NULL
1430
+ )
1431
+ SELECT source_ref, source_hash, any_hashed FROM ranked WHERE rn = 1`,
1432
+ [entityId, ...chunk]
1433
+ );
1434
+ for (const r of rows) out.set(r.source_ref, { latestHash: r.source_hash, anyHashed: Number(r.any_hashed) === 1 });
1435
+ }
1436
+ return out;
1437
+ }
1399
1438
  /**
1400
1439
  * Return the live source_refs for an entity that hold the given source_hash.
1401
1440
  * Used by the ingestDocument duplicate-hash guard and by hosts auditing
@@ -2655,6 +2694,105 @@ var GraphTraversalService = class {
2655
2694
  return { nodes, edges: filteredEdges };
2656
2695
  }
2657
2696
  };
2697
+
2698
+ // src/repositories/LintRepository.ts
2699
+ var LintRepository = class extends BaseRepository {
2700
+ async countFactHealth(entityId) {
2701
+ const row = await this.db.getFirstAsync(
2702
+ `SELECT
2703
+ SUM(CASE WHEN okf_type IS NULL THEN 1 ELSE 0 END) AS untyped,
2704
+ SUM(CASE WHEN lifecycle_status = 'draft' THEN 1 ELSE 0 END) AS drafts,
2705
+ SUM(CASE WHEN source_type = 'librarian_inferred'
2706
+ AND COALESCE(json_array_length(CASE WHEN json_valid(okf_verified) THEN okf_verified END), 0) = 0
2707
+ THEN 1 ELSE 0 END) AS unverified
2708
+ FROM ${this.prefix}entries
2709
+ WHERE entity_id = ? AND deleted_at IS NULL`,
2710
+ [entityId]
2711
+ );
2712
+ return {
2713
+ untypedFacts: Number(row?.untyped ?? 0),
2714
+ drafts: Number(row?.drafts ?? 0),
2715
+ unverifiedInferred: Number(row?.unverified ?? 0)
2716
+ };
2717
+ }
2718
+ danglingWhere() {
2719
+ const live = (col) => `EXISTS (SELECT 1 FROM ${this.prefix}entries n WHERE n.id = e.${col} AND n.entity_id = e.entity_id AND n.deleted_at IS NULL)`;
2720
+ return `e.entity_id = ? AND (NOT ${live("source_id")} OR NOT ${live("target_id")})`;
2721
+ }
2722
+ async countDanglingEdges(entityId) {
2723
+ const row = await this.db.getFirstAsync(
2724
+ `SELECT COUNT(*) AS n FROM ${this.prefix}edges e WHERE ${this.danglingWhere()}`,
2725
+ [entityId]
2726
+ );
2727
+ return Number(row?.n ?? 0);
2728
+ }
2729
+ async sampleDanglingEdgeIds(entityId, limit) {
2730
+ const rows = await this.db.getAllAsync(
2731
+ `SELECT e.id FROM ${this.prefix}edges e WHERE ${this.danglingWhere()} ORDER BY e.id ASC LIMIT ?`,
2732
+ [entityId, limit]
2733
+ );
2734
+ return rows.map((r) => r.id);
2735
+ }
2736
+ /** Keyset page of edges whose endpoints are both live in this entity, with endpoint types. */
2737
+ async pageLiveEdges(entityId, afterId, limit) {
2738
+ return this.db.getAllAsync(
2739
+ `SELECT e.id, e.edge_type, s.okf_type AS source_type, t.okf_type AS target_type
2740
+ FROM ${this.prefix}edges e
2741
+ JOIN ${this.prefix}entries s ON s.id = e.source_id AND s.entity_id = e.entity_id AND s.deleted_at IS NULL
2742
+ JOIN ${this.prefix}entries t ON t.id = e.target_id AND t.entity_id = e.entity_id AND t.deleted_at IS NULL
2743
+ WHERE e.entity_id = ? AND e.id > ?
2744
+ ORDER BY e.id ASC
2745
+ LIMIT ?`,
2746
+ [entityId, afterId, limit]
2747
+ );
2748
+ }
2749
+ };
2750
+
2751
+ // src/services/LintService.ts
2752
+ var LINT_SAMPLE_SIZE = 20;
2753
+ var LINT_PAGE_SIZE = 500;
2754
+ var LintService = class {
2755
+ constructor(lintRepo, ontologyService) {
2756
+ this.lintRepo = lintRepo;
2757
+ this.ontologyService = ontologyService;
2758
+ }
2759
+ /**
2760
+ * Read-only (spec §8.1). Manifest violations stream the entity's live-endpoint
2761
+ * edges in keyset pages and check each triple in JS against the effective
2762
+ * manifest. `getEffectiveState` is called without a transaction, so a seed
2763
+ * manifest is cached, never persisted. Counts come from separate statements,
2764
+ * not one snapshot.
2765
+ */
2766
+ async lint(entityId) {
2767
+ const health = await this.lintRepo.countFactHealth(entityId);
2768
+ const danglingEdges = await this.lintRepo.countDanglingEdges(entityId);
2769
+ const danglingEdgeIds = danglingEdges > 0 ? await this.lintRepo.sampleDanglingEdgeIds(entityId, LINT_SAMPLE_SIZE) : [];
2770
+ const { mode, manifest } = await this.ontologyService.getEffectiveState(entityId);
2771
+ const constrained = mode !== "off" && ((manifest.node_types?.length ?? 0) > 0 || (manifest.edge_types?.length ?? 0) > 0);
2772
+ let manifestViolations = 0;
2773
+ const manifestViolationEdgeIds = [];
2774
+ if (constrained) {
2775
+ let after = "";
2776
+ for (; ; ) {
2777
+ const page = await this.lintRepo.pageLiveEdges(entityId, after, LINT_PAGE_SIZE);
2778
+ for (const e of page) {
2779
+ if (!edgeTripleAllowed(manifest, e.edge_type, e.source_type ?? "", e.target_type ?? "")) {
2780
+ manifestViolations++;
2781
+ if (manifestViolationEdgeIds.length < LINT_SAMPLE_SIZE) manifestViolationEdgeIds.push(e.id);
2782
+ }
2783
+ }
2784
+ if (page.length < LINT_PAGE_SIZE) break;
2785
+ after = page[page.length - 1].id;
2786
+ }
2787
+ }
2788
+ return {
2789
+ danglingEdges,
2790
+ manifestViolations,
2791
+ ...health,
2792
+ sample: { danglingEdgeIds, manifestViolationEdgeIds }
2793
+ };
2794
+ }
2795
+ };
2658
2796
  var OkfTrustWritesRepository = class {
2659
2797
  constructor(db, prefix) {
2660
2798
  this.db = db;
@@ -2845,11 +2983,13 @@ var WikiMemory = class {
2845
2983
  this.eventRepo = new EventRepository(this.db, this.prefix);
2846
2984
  this.edgeRepo = new EdgeRepository(this.db, this.prefix);
2847
2985
  this.metadataRepo = new MetadataRepository(this.db, this.prefix);
2986
+ this.lintRepo = new LintRepository(this.db, this.prefix);
2848
2987
  this.ontologyService = new OntologyService(
2849
2988
  this.metadataRepo,
2850
2989
  this.edgeRepo,
2851
2990
  options.config?.ontology
2852
2991
  );
2992
+ this.lintService = new LintService(this.lintRepo, this.ontologyService);
2853
2993
  this.embeddingService = new EmbeddingService(this.db, this.options, this.entryRepo, this.metadataRepo);
2854
2994
  this.searchService = new SearchService(this.entryRepo);
2855
2995
  this.jobManager = new JobManager(this.prefix);
@@ -3032,6 +3172,61 @@ var WikiMemory = class {
3032
3172
  return { sourceRef: e.rawSourceRef, changed, duplicateOf: canonical };
3033
3173
  });
3034
3174
  }
3175
+ /**
3176
+ * Batch pending state per source (spec §8.2). `current` exactly when
3177
+ * `hasChanged` is false; `partial` when live rows exist but none carries a
3178
+ * hash (a first ingest with a failed chunk, or imported rows without a
3179
+ * hash). A failed re-ingest of an already-hashed ref reports `changed`. Validation and raw-ref echo match
3180
+ * batched `hasChanged`. Input order and duplicates are preserved. A
3181
+ * non-array `sources`, or an entry that is not an object (including a hole
3182
+ * in a sparse array), throws a `TypeError` before any SQL runs.
3183
+ */
3184
+ async pendingSources(entityId, sources) {
3185
+ assertEntityId(entityId);
3186
+ if (!Array.isArray(sources)) {
3187
+ throw new TypeError("Invalid sources: must be an array of { sourceRef, sourceHash } objects.");
3188
+ }
3189
+ if (sources.length === 0) return [];
3190
+ const normalized = Array.from(sources, (s, i) => {
3191
+ if (s === null || typeof s !== "object") {
3192
+ throw new TypeError(`Invalid sources[${i}]: must be a { sourceRef, sourceHash } object.`);
3193
+ }
3194
+ const { sourceRef: rawSourceRef, sourceHash: rawSourceHash } = s;
3195
+ const sourceRef = normalizeSourceRef(rawSourceRef);
3196
+ if (!sourceRef) {
3197
+ const shown = typeof rawSourceRef === "string" ? JSON.stringify(rawSourceRef) : `<${typeof rawSourceRef}>`;
3198
+ throw new Error(`Invalid sourceRef: ${shown}`);
3199
+ }
3200
+ const sourceHash = normalizeSourceHash(rawSourceHash);
3201
+ if (!sourceHash) throw new Error("Invalid sourceHash: must be a 64-character hex string (normalized to lowercase)");
3202
+ return { rawSourceRef, sourceRef, sourceHash };
3203
+ });
3204
+ const states = await this.entryRepo.findSourceStates(entityId, normalized.map((n) => n.sourceRef));
3205
+ return normalized.map((n) => {
3206
+ const state = states.get(n.sourceRef);
3207
+ let status;
3208
+ if (!state) status = "new";
3209
+ else if (!state.anyHashed) status = "partial";
3210
+ else if (state.latestHash !== null && normalizeSourceHash(state.latestHash) === n.sourceHash) status = "current";
3211
+ else status = "changed";
3212
+ return { sourceRef: n.rawSourceRef, status };
3213
+ });
3214
+ }
3215
+ /** Read-only maintenance report for one entity (spec §8.1). Reports, never repairs. */
3216
+ async lint(entityId) {
3217
+ assertEntityId(entityId);
3218
+ return this.lintService.lint(entityId);
3219
+ }
3220
+ /**
3221
+ * Effective system prompts for ingest, librarian, heal and ontology backfill
3222
+ * (spec §8.3). Backs the `wiki_get_instructions` tool. Templates only;
3223
+ * overrides are returned verbatim, so keep secrets out of `WikiConfig.prompts`.
3224
+ */
3225
+ async getInstructions(entityId) {
3226
+ assertEntityId(entityId);
3227
+ const ontologyContext = await this.ontologyService.buildPromptContext(entityId);
3228
+ return this.promptService.buildInstructionTemplates(ontologyContext);
3229
+ }
3035
3230
  /**
3036
3231
  * Returns the live source_refs for an entity, one row per ref, with the most
3037
3232
  * recently-updated live `source_hash` and a live fact count. Refs are sorted
@@ -3432,6 +3627,11 @@ var WikiMemory = class {
3432
3627
  }
3433
3628
  };
3434
3629
  _testAccessNonTestEnvWarned = new WeakMap();
3630
+ function assertEntityId(entityId) {
3631
+ if (typeof entityId !== "string" || entityId.length === 0) {
3632
+ throw new TypeError("Invalid entityId: must be a non-empty string.");
3633
+ }
3634
+ }
3435
3635
 
3436
3636
  // src/utils/formatContext.ts
3437
3637
  function validateMaxOption(value, name) {