@opengeni/documents 0.5.18 → 0.5.38

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.js CHANGED
@@ -8,9 +8,185 @@ import {
8
8
  withWorkspaceSubjectRls
9
9
  } from "@opengeni/db";
10
10
  import * as schema from "@opengeni/db/schema";
11
- import { LiteParse } from "@llamaindex/liteparse";
12
- import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm";
13
- import OpenAI from "openai";
11
+ import { createHash } from "crypto";
12
+ import { and, asc, desc, eq, gt, inArray, isNotNull, or, sql } from "drizzle-orm";
13
+ import { KNOWLEDGE_BROWSE_CURSOR_MAX_CHARS } from "@opengeni/contracts";
14
+
15
+ // src/knowledge-projection.ts
16
+ import {
17
+ KNOWLEDGE_BODY_MAX_BYTES,
18
+ KNOWLEDGE_METADATA_MAX_BYTES,
19
+ KNOWLEDGE_METADATA_MAX_DEPTH,
20
+ KNOWLEDGE_METADATA_MAX_ITEMS,
21
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
22
+ KNOWLEDGE_SOURCE_URI_MAX_BYTES,
23
+ KNOWLEDGE_SUMMARY_MAX_BYTES,
24
+ KNOWLEDGE_TITLE_MAX_BYTES,
25
+ KNOWLEDGE_TOPIC_MAX_BYTES,
26
+ KNOWLEDGE_TOPICS_MAX_ITEMS
27
+ } from "@opengeni/contracts";
28
+ var utf8Bytes = (value) => Buffer.byteLength(value, "utf8");
29
+ function truncateUtf8(value, maxBytes) {
30
+ if (utf8Bytes(value) <= maxBytes) return { value, truncated: false };
31
+ let low = 0;
32
+ let high = value.length;
33
+ while (low < high) {
34
+ const middle = Math.ceil((low + high) / 2);
35
+ const end2 = middle > 0 && middle < value.length && /[\uD800-\uDBFF]/u.test(value[middle - 1]) ? middle - 1 : middle;
36
+ if (utf8Bytes(value.slice(0, end2)) <= maxBytes) low = middle;
37
+ else high = middle - 1;
38
+ }
39
+ let end = low;
40
+ if (end > 0 && end < value.length && /[\uD800-\uDBFF]/u.test(value[end - 1])) end -= 1;
41
+ while (end > 0 && utf8Bytes(value.slice(0, end)) > maxBytes) end -= 1;
42
+ return { value: value.slice(0, end), truncated: true };
43
+ }
44
+ function projectNullableString(value, maxBytes, field, fields) {
45
+ if (value === null) return null;
46
+ const projected = truncateUtf8(value, maxBytes);
47
+ if (projected.truncated) fields.add(field);
48
+ return projected.value;
49
+ }
50
+ function projectSourceUri(value, fields) {
51
+ if (value === null || value === "") return null;
52
+ if (utf8Bytes(value) > KNOWLEDGE_SOURCE_URI_MAX_BYTES) {
53
+ fields.add("provenance.source.uri");
54
+ return null;
55
+ }
56
+ return value;
57
+ }
58
+ function projectTopics(value, fields) {
59
+ if (!Array.isArray(value)) {
60
+ if (value !== null && value !== void 0) fields.add("content.topics");
61
+ return [];
62
+ }
63
+ const result = [];
64
+ for (const candidate of value) {
65
+ if (result.length >= KNOWLEDGE_TOPICS_MAX_ITEMS) {
66
+ fields.add("content.topics");
67
+ break;
68
+ }
69
+ if (typeof candidate !== "string") {
70
+ fields.add("content.topics");
71
+ continue;
72
+ }
73
+ const projected = truncateUtf8(candidate, KNOWLEDGE_TOPIC_MAX_BYTES);
74
+ if (projected.truncated) fields.add("content.topics");
75
+ result.push(projected.value);
76
+ }
77
+ return result;
78
+ }
79
+ function projectMetadata(value) {
80
+ let remainingItems = KNOWLEDGE_METADATA_MAX_ITEMS;
81
+ let truncated = false;
82
+ const visit = (candidate, depth) => {
83
+ if (remainingItems <= 0 || depth > KNOWLEDGE_METADATA_MAX_DEPTH) {
84
+ truncated = true;
85
+ return void 0;
86
+ }
87
+ remainingItems -= 1;
88
+ if (candidate === null || typeof candidate === "boolean" || typeof candidate === "number" && Number.isFinite(candidate)) {
89
+ return candidate;
90
+ }
91
+ if (typeof candidate === "string") {
92
+ const projected = truncateUtf8(candidate, KNOWLEDGE_SOURCE_STRING_MAX_BYTES);
93
+ if (projected.truncated) truncated = true;
94
+ return projected.value;
95
+ }
96
+ if (Array.isArray(candidate)) {
97
+ const result2 = [];
98
+ for (const item of candidate) {
99
+ const projected = visit(item, depth + 1);
100
+ if (projected === void 0) break;
101
+ result2.push(projected);
102
+ }
103
+ return result2;
104
+ }
105
+ if (candidate && typeof candidate === "object") {
106
+ const result2 = {};
107
+ for (const key of Object.keys(candidate).sort()) {
108
+ if (utf8Bytes(key) > KNOWLEDGE_TOPIC_MAX_BYTES) {
109
+ truncated = true;
110
+ continue;
111
+ }
112
+ const projected = visit(candidate[key], depth + 1);
113
+ if (projected === void 0) break;
114
+ result2[key] = projected;
115
+ }
116
+ return result2;
117
+ }
118
+ truncated = true;
119
+ return void 0;
120
+ };
121
+ const root = visit(value, 0);
122
+ let result = root && typeof root === "object" && !Array.isArray(root) ? root : {};
123
+ if (result !== root) truncated = true;
124
+ const bounded = {};
125
+ for (const key of Object.keys(result).sort()) {
126
+ bounded[key] = result[key];
127
+ if (utf8Bytes(JSON.stringify(bounded)) > KNOWLEDGE_METADATA_MAX_BYTES) {
128
+ delete bounded[key];
129
+ truncated = true;
130
+ break;
131
+ }
132
+ }
133
+ result = bounded;
134
+ return { value: result, truncated };
135
+ }
136
+ function projectKnowledgeRecord(input) {
137
+ const fields = /* @__PURE__ */ new Set();
138
+ const title = truncateUtf8(input.title, KNOWLEDGE_TITLE_MAX_BYTES);
139
+ if (title.truncated) fields.add("title");
140
+ const metadata = projectMetadata(input.metadata);
141
+ if (metadata.truncated) fields.add("content.metadata");
142
+ const source = {
143
+ ...input.source,
144
+ uri: projectSourceUri(input.source.uri, fields),
145
+ externalId: projectNullableString(
146
+ input.source.externalId,
147
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
148
+ "provenance.source.externalId",
149
+ fields
150
+ ),
151
+ title: projectNullableString(
152
+ input.source.title,
153
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
154
+ "provenance.source.title",
155
+ fields
156
+ ),
157
+ author: projectNullableString(
158
+ input.source.author,
159
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
160
+ "provenance.source.author",
161
+ fields
162
+ ),
163
+ version: projectNullableString(
164
+ input.source.version,
165
+ KNOWLEDGE_SOURCE_STRING_MAX_BYTES,
166
+ "provenance.source.version",
167
+ fields
168
+ )
169
+ };
170
+ return {
171
+ title: title.value,
172
+ content: {
173
+ format: "markdown",
174
+ body: projectNullableString(input.body, KNOWLEDGE_BODY_MAX_BYTES, "content.body", fields),
175
+ summary: projectNullableString(
176
+ input.summary,
177
+ KNOWLEDGE_SUMMARY_MAX_BYTES,
178
+ "content.summary",
179
+ fields
180
+ ),
181
+ topics: projectTopics(input.topics, fields),
182
+ metadata: metadata.value
183
+ },
184
+ source,
185
+ projection: { truncated: fields.size > 0, fields: [...fields].sort() }
186
+ };
187
+ }
188
+
189
+ // src/index.ts
14
190
  var DEFAULT_DOCUMENT_PARSER = "liteparse";
15
191
  var DEFAULT_DOCUMENT_EMBEDDING_MODEL = "text-embedding-3-large";
16
192
  var DEFAULT_DOCUMENT_EMBEDDING_DIMENSIONS = 3072;
@@ -19,6 +195,7 @@ var DEFAULT_DOCUMENT_CHUNK_OVERLAP = 160;
19
195
  var DEFAULT_DOCUMENT_CURATION_MODEL = "gpt-4o-mini";
20
196
  var DOCUMENT_CURATION_MAX_INPUT_CHARS = 24e3;
21
197
  var DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES = 1024;
198
+ var DOCUMENT_INDEX_CHECKPOINT_MAX_CHARS = 1024;
22
199
  var DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE = 0.75;
23
200
  var DEFAULT_BASE_NAME = "Default";
24
201
  var DEFAULT_BASE_DESCRIPTION = "Default base for dropped files and notes.";
@@ -44,6 +221,7 @@ var LiteParseDocumentParser = class {
44
221
  }
45
222
  async parseWithLiteParse(bytes) {
46
223
  return await this.enqueueParse(async () => {
224
+ const { LiteParse } = await import("@llamaindex/liteparse");
47
225
  const parser = new LiteParse({ ocrEnabled: true, numWorkers: 1 });
48
226
  const result = await parser.parse(Buffer.from(bytes), true);
49
227
  const text = typeof result?.text === "string" ? result.text : "";
@@ -85,7 +263,7 @@ var RecursiveTextChunker = class {
85
263
  }
86
264
  };
87
265
  var OpenAIEmbeddingProvider = class {
88
- client = null;
266
+ clientPromise = null;
89
267
  apiKey;
90
268
  baseURL;
91
269
  constructor(args) {
@@ -105,7 +283,7 @@ var OpenAIEmbeddingProvider = class {
105
283
  const out = [];
106
284
  for (let start = 0; start < texts.length; start += 64) {
107
285
  const batch = texts.slice(start, start + 64);
108
- const response = await this.openai().embeddings.create({
286
+ const response = await (await this.openai()).embeddings.create({
109
287
  model: this.model,
110
288
  input: batch,
111
289
  dimensions: this.dimensions
@@ -123,17 +301,19 @@ var OpenAIEmbeddingProvider = class {
123
301
  }
124
302
  return embedding;
125
303
  }
126
- openai() {
304
+ async openai() {
127
305
  if (!this.apiKey) {
128
306
  throw new Error("OpenAI document embeddings require an API key");
129
307
  }
130
- this.client ??= new OpenAI({
131
- apiKey: this.apiKey,
132
- ...this.baseURL ? { baseURL: this.baseURL } : {},
133
- ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
134
- ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
135
- });
136
- return this.client;
308
+ this.clientPromise ??= import("openai").then(
309
+ ({ default: OpenAIClient }) => new OpenAIClient({
310
+ apiKey: this.apiKey,
311
+ ...this.baseURL ? { baseURL: this.baseURL } : {},
312
+ ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
313
+ ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
314
+ })
315
+ );
316
+ return await this.clientPromise;
137
317
  }
138
318
  };
139
319
  var DeterministicEmbeddingProvider = class {
@@ -196,7 +376,7 @@ var CURATION_SYSTEM_PROMPT = [
196
376
  "well, return targetBaseId null and confidence 0. Respond with JSON only."
197
377
  ].join(" ");
198
378
  var OpenAICurationProvider = class {
199
- client = null;
379
+ clientPromise = null;
200
380
  apiKey;
201
381
  baseURL;
202
382
  defaultHeaders;
@@ -210,7 +390,7 @@ var OpenAICurationProvider = class {
210
390
  this.model = args.model ?? DEFAULT_DOCUMENT_CURATION_MODEL;
211
391
  }
212
392
  async curate(input) {
213
- const response = await this.openai().chat.completions.create({
393
+ const response = await (await this.openai()).chat.completions.create({
214
394
  model: this.model,
215
395
  response_format: { type: "json_object" },
216
396
  messages: [
@@ -232,17 +412,19 @@ var OpenAICurationProvider = class {
232
412
  }
233
413
  return parseCurationOutcome(raw, input.bases);
234
414
  }
235
- openai() {
415
+ async openai() {
236
416
  if (!this.apiKey) {
237
417
  throw new Error("OpenAI document curation requires an API key");
238
418
  }
239
- this.client ??= new OpenAI({
240
- apiKey: this.apiKey,
241
- ...this.baseURL ? { baseURL: this.baseURL } : {},
242
- ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
243
- ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
244
- });
245
- return this.client;
419
+ this.clientPromise ??= import("openai").then(
420
+ ({ default: OpenAIClient }) => new OpenAIClient({
421
+ apiKey: this.apiKey,
422
+ ...this.baseURL ? { baseURL: this.baseURL } : {},
423
+ ...this.defaultQuery ? { defaultQuery: this.defaultQuery } : {},
424
+ ...this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}
425
+ })
426
+ );
427
+ return await this.clientPromise;
246
428
  }
247
429
  };
248
430
  function parseCurationOutcome(raw, bases) {
@@ -534,15 +716,24 @@ async function addDocumentToBase(db, input) {
534
716
  const base = await getDocumentBase(scopedDb, input.workspaceId, input.baseId);
535
717
  if (!base) throw new Error(`Document base not found: ${input.baseId}`);
536
718
  const file = await requireReadyFile(scopedDb, input.workspaceId, input.fileId);
719
+ const knowledgeSourceIdentity = cleanString(input.knowledgeSourceIdentity ?? null);
720
+ if (knowledgeSourceIdentity && knowledgeSourceIdentity.length > 512) {
721
+ throw new Error("knowledge source document identity exceeds 512 characters");
722
+ }
537
723
  const now = /* @__PURE__ */ new Date();
538
724
  const [existing] = await scopedDb.select().from(schema.documents).where(
539
725
  and(
540
726
  eq(schema.documents.workspaceId, input.workspaceId),
541
- eq(schema.documents.baseId, input.baseId),
542
- eq(schema.documents.fileId, input.fileId)
727
+ ...knowledgeSourceIdentity ? [eq(schema.documents.knowledgeSourceIdentity, knowledgeSourceIdentity)] : [
728
+ eq(schema.documents.baseId, input.baseId),
729
+ eq(schema.documents.fileId, input.fileId)
730
+ ]
543
731
  )
544
732
  ).limit(1);
545
733
  if (existing) {
734
+ if (knowledgeSourceIdentity && existing.fileId !== input.fileId) {
735
+ throw new Error("knowledge source document identity is bound to different content");
736
+ }
546
737
  if (!documentMatchesAccess(existing, input.workspaceId, input.access)) {
547
738
  throw new Error(`Document not found: ${existing.id}`);
548
739
  }
@@ -587,6 +778,7 @@ async function addDocumentToBase(db, input) {
587
778
  sourceCreatedAt: parseOptionalDate(input.sourceCreatedAt),
588
779
  sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
589
780
  sourceVersion: cleanString(input.sourceVersion) ?? null,
781
+ knowledgeSourceIdentity,
590
782
  aclTags: cleanStringArray(input.aclTags),
591
783
  authorityKind: authority.kind,
592
784
  authorityWorkspaceId: authority.workspaceId,
@@ -630,7 +822,7 @@ async function moveDocumentToBase(db, input) {
630
822
  and(
631
823
  eq(schema.documents.workspaceId, input.workspaceId),
632
824
  eq(schema.documents.baseId, targetBaseId),
633
- eq(schema.documents.fileId, row.fileId)
825
+ ...row.knowledgeSourceIdentity ? [eq(schema.documents.knowledgeSourceIdentity, row.knowledgeSourceIdentity)] : [eq(schema.documents.fileId, row.fileId)]
634
826
  )
635
827
  ).limit(1);
636
828
  if (conflict) {
@@ -710,6 +902,101 @@ async function listDocuments(db, workspaceId, baseId, access) {
710
902
  return rows.map(mapDocument);
711
903
  });
712
904
  }
905
+ async function listEffectiveIndexedDocuments(db, input) {
906
+ const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
907
+ const limit = input.limit ?? 50;
908
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
909
+ throw new Error("indexed document list limit must be between 1 and 100");
910
+ }
911
+ const afterSequence = input.checkpoint ? decodeDocumentIndexCheckpoint(input.checkpoint, {
912
+ accountId: input.accountId,
913
+ workspaceId: input.workspaceId,
914
+ initiatingSubjectId
915
+ }) : 0n;
916
+ const access = {
917
+ agentOnly: true,
918
+ viewerSubjectId: initiatingSubjectId
919
+ };
920
+ const rows = await withDocumentAccountRls(
921
+ db,
922
+ input.accountId,
923
+ input.workspaceId,
924
+ access,
925
+ async (scopedDb) => await scopedDb.select().from(schema.documents).where(
926
+ and(
927
+ eq(schema.documents.accountId, input.accountId),
928
+ eq(schema.documents.status, "ready"),
929
+ isNotNull(schema.documents.indexSequence),
930
+ gt(schema.documents.indexSequence, afterSequence),
931
+ ...documentAccessConditions(input.workspaceId, access)
932
+ )
933
+ ).orderBy(asc(schema.documents.indexSequence)).limit(limit + 1)
934
+ );
935
+ const hasMore = rows.length > limit;
936
+ const pageRows = rows.slice(0, limit);
937
+ const nextSequence = pageRows.at(-1)?.indexSequence ?? afterSequence;
938
+ if (nextSequence === null) {
939
+ throw new Error("ready document is missing its index sequence");
940
+ }
941
+ return {
942
+ documents: pageRows.map(mapIndexedDocumentSummary),
943
+ nextCheckpoint: encodeDocumentIndexCheckpoint({
944
+ accountId: input.accountId,
945
+ workspaceId: input.workspaceId,
946
+ initiatingSubjectId,
947
+ sequence: nextSequence
948
+ }),
949
+ hasMore
950
+ };
951
+ }
952
+ function encodeDocumentIndexCheckpoint(input) {
953
+ if (input.sequence < 0n) throw new Error("document index checkpoint sequence is invalid");
954
+ return Buffer.from(
955
+ JSON.stringify({
956
+ v: 1,
957
+ s: documentIndexCheckpointScope(input),
958
+ q: input.sequence.toString()
959
+ }),
960
+ "utf8"
961
+ ).toString("base64url");
962
+ }
963
+ function decodeDocumentIndexCheckpoint(value, scope) {
964
+ try {
965
+ if (!value || value.length > DOCUMENT_INDEX_CHECKPOINT_MAX_CHARS) {
966
+ throw new Error("checkpoint length");
967
+ }
968
+ const bytes = Buffer.from(value, "base64url");
969
+ if (bytes.toString("base64url") !== value) throw new Error("checkpoint encoding");
970
+ const parsed = JSON.parse(bytes.toString("utf8"));
971
+ if (Object.keys(parsed).sort().join(",") !== "q,s,v" || parsed.v !== 1 || typeof parsed.s !== "string" || typeof parsed.q !== "string" || !/^(0|[1-9][0-9]*)$/.test(parsed.q)) {
972
+ throw new Error("checkpoint payload");
973
+ }
974
+ if (parsed.s !== documentIndexCheckpointScope(scope)) {
975
+ throw new Error("document index checkpoint belongs to a different workspace or subject");
976
+ }
977
+ return BigInt(parsed.q);
978
+ } catch (error) {
979
+ if (error instanceof Error && error.message === "document index checkpoint belongs to a different workspace or subject") {
980
+ throw error;
981
+ }
982
+ throw new Error("invalid document index checkpoint", { cause: error });
983
+ }
984
+ }
985
+ function documentIndexCheckpointScope(input) {
986
+ return createHash("sha256").update("opengeni:document-index-checkpoint:v1\0").update(input.accountId).update("\0").update(input.workspaceId).update("\0").update(canonicalEffectiveDocumentSubject(input.initiatingSubjectId)).digest("hex");
987
+ }
988
+ function canonicalEffectiveDocumentSubject(value) {
989
+ const subjectId = cleanString(value);
990
+ if (!subjectId || subjectId !== value) {
991
+ throw new Error("effective document retrieval requires an initiating subject");
992
+ }
993
+ if (new TextEncoder().encode(subjectId).byteLength > DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES) {
994
+ throw new Error(
995
+ `effective document initiating subject exceeds ${DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES} UTF-8 bytes`
996
+ );
997
+ }
998
+ return subjectId;
999
+ }
713
1000
  async function getDocument(db, workspaceId, documentId, access) {
714
1001
  return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
715
1002
  const [row] = await scopedDb.select().from(schema.documents).where(
@@ -1024,6 +1311,293 @@ async function searchEffectiveDocuments(db, input, services = createDocumentServ
1024
1311
  services
1025
1312
  );
1026
1313
  }
1314
+ async function searchEffectiveKnowledge(db, input, services = createDocumentServices()) {
1315
+ const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
1316
+ const ranked = await searchEffectiveDocuments(
1317
+ db,
1318
+ { ...input, initiatingSubjectId, surface: "agent" },
1319
+ services
1320
+ );
1321
+ if (ranked.length === 0) return { results: [] };
1322
+ const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
1323
+ const current = await withDocumentAccountRls(
1324
+ db,
1325
+ input.accountId,
1326
+ input.workspaceId,
1327
+ access,
1328
+ async (scopedDb) => await scopedDb.select({ chunk: schema.documentChunks, document: schema.documents }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1329
+ and(
1330
+ eq(schema.documents.accountId, input.accountId),
1331
+ eq(schema.documentChunks.accountId, input.accountId),
1332
+ inArray(
1333
+ schema.documentChunks.id,
1334
+ ranked.map((result) => result.chunkId)
1335
+ ),
1336
+ eq(schema.documents.status, "ready"),
1337
+ ...documentAccessConditions(input.workspaceId, access)
1338
+ )
1339
+ )
1340
+ );
1341
+ const currentByChunkId = new Map(current.map((row) => [row.chunk.id, row]));
1342
+ return {
1343
+ results: ranked.flatMap((rankedResult) => {
1344
+ const row = currentByChunkId.get(rankedResult.chunkId);
1345
+ if (!row) return [];
1346
+ return [
1347
+ {
1348
+ record: knowledgeChunkRecord(row.document, row.chunk),
1349
+ retrieval: {
1350
+ score: rankedResult.score,
1351
+ matchType: rankedResult.matchType,
1352
+ vectorScore: rankedResult.vectorScore,
1353
+ keywordScore: rankedResult.keywordScore
1354
+ }
1355
+ }
1356
+ ];
1357
+ })
1358
+ };
1359
+ }
1360
+ async function getEffectiveKnowledgeRecord(db, input) {
1361
+ const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
1362
+ const target = parseKnowledgeRecordId(input.id);
1363
+ const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
1364
+ return await withDocumentAccountRls(
1365
+ db,
1366
+ input.accountId,
1367
+ input.workspaceId,
1368
+ access,
1369
+ async (scopedDb) => {
1370
+ if (target.kind === "document") {
1371
+ const [document] = await scopedDb.select().from(schema.documents).where(
1372
+ and(
1373
+ eq(schema.documents.accountId, input.accountId),
1374
+ eq(schema.documents.id, target.id),
1375
+ eq(schema.documents.status, "ready"),
1376
+ ...documentAccessConditions(input.workspaceId, access)
1377
+ )
1378
+ ).limit(1);
1379
+ return document ? knowledgeDocumentRecord(document) : null;
1380
+ }
1381
+ const [row] = await scopedDb.select({ chunk: schema.documentChunks, document: schema.documents }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1382
+ and(
1383
+ eq(schema.documents.accountId, input.accountId),
1384
+ eq(schema.documentChunks.accountId, input.accountId),
1385
+ eq(schema.documentChunks.id, target.id),
1386
+ eq(schema.documents.status, "ready"),
1387
+ ...documentAccessConditions(input.workspaceId, access)
1388
+ )
1389
+ ).limit(1);
1390
+ return row ? knowledgeChunkRecord(row.document, row.chunk) : null;
1391
+ }
1392
+ );
1393
+ }
1394
+ async function browseEffectiveKnowledge(db, input) {
1395
+ const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
1396
+ const limit = input.limit ?? 20;
1397
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
1398
+ throw new Error("knowledge browse limit must be between 1 and 50");
1399
+ }
1400
+ const topic = input.topic === void 0 ? null : cleanString(input.topic) ?? null;
1401
+ if (input.topic !== void 0 && (!topic || topic !== input.topic || topic.length > 256)) {
1402
+ throw new Error("knowledge browse topic is invalid");
1403
+ }
1404
+ const sourceKinds = [...new Set(input.sourceKinds ?? [])].sort();
1405
+ const parent = input.parentId ? parseKnowledgeRecordId(input.parentId) : null;
1406
+ if (parent?.kind === "document_chunk") {
1407
+ throw new Error("knowledge browse parent must be a document record");
1408
+ }
1409
+ if (parent && (topic || sourceKinds.length > 0)) {
1410
+ throw new Error("knowledge browse document contents do not accept topic/source filters");
1411
+ }
1412
+ const cursorScope = {
1413
+ accountId: input.accountId,
1414
+ workspaceId: input.workspaceId,
1415
+ initiatingSubjectId,
1416
+ parentId: parent ? `${parent.kind}:${parent.id}` : null,
1417
+ topic,
1418
+ sourceKinds
1419
+ };
1420
+ const after = input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, cursorScope) : 0n;
1421
+ if (parent && after > 2147483648n) {
1422
+ throw new Error("invalid knowledge browse cursor");
1423
+ }
1424
+ const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
1425
+ return await withDocumentAccountRls(
1426
+ db,
1427
+ input.accountId,
1428
+ input.workspaceId,
1429
+ access,
1430
+ async (scopedDb) => {
1431
+ if (parent) {
1432
+ const [authorizedParent] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
1433
+ and(
1434
+ eq(schema.documents.accountId, input.accountId),
1435
+ eq(schema.documents.id, parent.id),
1436
+ eq(schema.documents.status, "ready"),
1437
+ ...documentAccessConditions(input.workspaceId, access)
1438
+ )
1439
+ ).limit(1);
1440
+ if (!authorizedParent) return { records: [], nextCursor: null, hasMore: false };
1441
+ const rows2 = await scopedDb.select({ chunk: schema.documentChunks, document: schema.documents }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1442
+ and(
1443
+ eq(schema.documentChunks.accountId, input.accountId),
1444
+ eq(schema.documentChunks.documentId, parent.id),
1445
+ gt(schema.documentChunks.chunkIndex, Number(after) - 1),
1446
+ eq(schema.documents.status, "ready"),
1447
+ ...documentAccessConditions(input.workspaceId, access)
1448
+ )
1449
+ ).orderBy(asc(schema.documentChunks.chunkIndex)).limit(limit + 1);
1450
+ const hasMore2 = rows2.length > limit;
1451
+ const page2 = rows2.slice(0, limit);
1452
+ const last2 = page2.at(-1)?.chunk.chunkIndex;
1453
+ return {
1454
+ records: page2.map((row) => knowledgeChunkRecord(row.document, row.chunk)),
1455
+ nextCursor: hasMore2 && last2 !== void 0 ? encodeKnowledgeBrowseCursor(cursorScope, BigInt(last2 + 1)) : null,
1456
+ hasMore: hasMore2
1457
+ };
1458
+ }
1459
+ const conditions = [
1460
+ eq(schema.documents.accountId, input.accountId),
1461
+ eq(schema.documents.status, "ready"),
1462
+ isNotNull(schema.documents.indexSequence),
1463
+ gt(schema.documents.indexSequence, after),
1464
+ ...documentAccessConditions(input.workspaceId, access)
1465
+ ];
1466
+ if (topic) conditions.push(sql`${schema.documents.topics} ? ${topic}`);
1467
+ if (sourceKinds.length > 0)
1468
+ conditions.push(inArray(schema.documents.sourceKind, sourceKinds));
1469
+ const rows = await scopedDb.select().from(schema.documents).where(and(...conditions)).orderBy(asc(schema.documents.indexSequence)).limit(limit + 1);
1470
+ const hasMore = rows.length > limit;
1471
+ const page = rows.slice(0, limit);
1472
+ const last = page.at(-1)?.indexSequence;
1473
+ return {
1474
+ records: page.map(knowledgeDocumentRecord),
1475
+ nextCursor: hasMore && last !== void 0 && last !== null ? encodeKnowledgeBrowseCursor(cursorScope, last) : null,
1476
+ hasMore
1477
+ };
1478
+ }
1479
+ );
1480
+ }
1481
+ function encodeKnowledgeBrowseCursor(scope, position) {
1482
+ if (position < 0n) throw new Error("knowledge browse cursor position is invalid");
1483
+ return Buffer.from(
1484
+ JSON.stringify({ v: 1, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
1485
+ "utf8"
1486
+ ).toString("base64url");
1487
+ }
1488
+ function decodeKnowledgeBrowseCursor(value, scope) {
1489
+ try {
1490
+ if (!value || value.length > KNOWLEDGE_BROWSE_CURSOR_MAX_CHARS) {
1491
+ throw new Error("cursor length");
1492
+ }
1493
+ const bytes = Buffer.from(value, "base64url");
1494
+ if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
1495
+ const parsed = JSON.parse(bytes.toString("utf8"));
1496
+ if (Object.keys(parsed).sort().join(",") !== "q,s,v" || parsed.v !== 1 || typeof parsed.s !== "string" || typeof parsed.q !== "string" || !/^(0|[1-9][0-9]*)$/.test(parsed.q)) {
1497
+ throw new Error("cursor payload");
1498
+ }
1499
+ if (parsed.s !== knowledgeBrowseCursorScope(scope)) {
1500
+ throw new Error("knowledge browse cursor belongs to a different scope");
1501
+ }
1502
+ const position = BigInt(parsed.q);
1503
+ if (position > 9223372036854775807n) throw new Error("cursor position");
1504
+ return position;
1505
+ } catch (error) {
1506
+ if (error instanceof Error && error.message === "knowledge browse cursor belongs to a different scope") {
1507
+ throw error;
1508
+ }
1509
+ throw new Error("invalid knowledge browse cursor", { cause: error });
1510
+ }
1511
+ }
1512
+ function knowledgeBrowseCursorScope(scope) {
1513
+ return createHash("sha256").update("opengeni:knowledge-browse-cursor:v1\0").update(scope.accountId).update("\0").update(scope.workspaceId).update("\0").update(canonicalEffectiveDocumentSubject(scope.initiatingSubjectId)).update("\0").update(scope.parentId ?? "").update("\0").update(scope.topic ?? "").update("\0").update([...scope.sourceKinds].sort().join("\0")).digest("hex");
1514
+ }
1515
+ function parseKnowledgeRecordId(value) {
1516
+ const match = /^(document|document_chunk):([0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/iu.exec(
1517
+ value
1518
+ );
1519
+ if (!match) throw new Error("invalid knowledge record id");
1520
+ return { kind: match[1], id: match[2].toLowerCase() };
1521
+ }
1522
+ function knowledgeDocumentRecord(document) {
1523
+ if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
1524
+ const projected = projectKnowledgeRecord({
1525
+ title: document.title,
1526
+ body: document.summary,
1527
+ summary: document.summary,
1528
+ topics: document.topics,
1529
+ metadata: { parser: document.parser, chunkCount: document.chunkCount },
1530
+ source: knowledgeSource(document)
1531
+ });
1532
+ return {
1533
+ id: `document:${document.id}`,
1534
+ kind: "document",
1535
+ title: projected.title,
1536
+ content: projected.content,
1537
+ authority: { kind: normalizeDocumentAuthorityKind(document.authorityKind) },
1538
+ provenance: {
1539
+ source: projected.source,
1540
+ indexedAt: document.indexedAt.toISOString()
1541
+ },
1542
+ lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
1543
+ quality: knowledgeQuality(document),
1544
+ links: knowledgeSourceLinks(projected.source.uri),
1545
+ projection: projected.projection
1546
+ };
1547
+ }
1548
+ function knowledgeChunkRecord(document, chunk) {
1549
+ if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
1550
+ const projected = projectKnowledgeRecord({
1551
+ title: document.title,
1552
+ body: chunk.text,
1553
+ summary: document.summary,
1554
+ topics: document.topics,
1555
+ metadata: { ...chunk.metadata, chunkIndex: chunk.chunkIndex },
1556
+ source: knowledgeSource(document)
1557
+ });
1558
+ return {
1559
+ id: `document_chunk:${chunk.id}`,
1560
+ kind: "document_chunk",
1561
+ title: projected.title,
1562
+ content: projected.content,
1563
+ authority: { kind: normalizeDocumentAuthorityKind(document.authorityKind) },
1564
+ provenance: {
1565
+ source: projected.source,
1566
+ indexedAt: document.indexedAt.toISOString()
1567
+ },
1568
+ lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
1569
+ quality: knowledgeQuality(document),
1570
+ links: [
1571
+ { relation: "parent", target: { kind: "knowledge", id: `document:${document.id}` } },
1572
+ ...knowledgeSourceLinks(projected.source.uri)
1573
+ ],
1574
+ projection: projected.projection
1575
+ };
1576
+ }
1577
+ function knowledgeSource(document) {
1578
+ return {
1579
+ kind: normalizeKnowledgeSourceKind(document.sourceKind),
1580
+ uri: document.sourceUri,
1581
+ externalId: document.sourceExternalId,
1582
+ title: document.sourceTitle,
1583
+ author: document.sourceAuthor,
1584
+ createdAt: document.sourceCreatedAt?.toISOString() ?? null,
1585
+ updatedAt: document.sourceUpdatedAt?.toISOString() ?? null,
1586
+ version: document.sourceVersion
1587
+ };
1588
+ }
1589
+ function knowledgeQuality(document) {
1590
+ if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
1591
+ return {
1592
+ trust: "sourced",
1593
+ freshnessAt: (document.sourceUpdatedAt ?? document.indexedAt).toISOString(),
1594
+ conflict: "not_evaluated",
1595
+ correction: "current_source_version"
1596
+ };
1597
+ }
1598
+ function knowledgeSourceLinks(sourceUri) {
1599
+ return sourceUri ? [{ relation: "source", target: { kind: "external", uri: sourceUri } }] : [];
1600
+ }
1027
1601
  async function vectorSearchDocuments(db, input, limit, services) {
1028
1602
  const queryEmbedding = await services.embedder.embedQuery(input.query);
1029
1603
  validateEmbedding(queryEmbedding, services.embedder.dimensions, services.embedder.model);
@@ -1508,6 +2082,40 @@ function mapDocument(row) {
1508
2082
  updatedAt: row.updatedAt.toISOString()
1509
2083
  };
1510
2084
  }
2085
+ function mapIndexedDocumentSummary(row) {
2086
+ if (row.indexSequence === null || row.indexedAt === null) {
2087
+ throw new Error(`Ready document is missing index completion metadata: ${row.id}`);
2088
+ }
2089
+ return {
2090
+ id: row.id,
2091
+ title: row.title,
2092
+ parser: row.parser,
2093
+ chunkCount: row.chunkCount,
2094
+ indexedAt: row.indexedAt.toISOString(),
2095
+ summary: row.summary,
2096
+ topics: cleanStringArray(row.topics),
2097
+ source: {
2098
+ kind: normalizeKnowledgeSourceKind(row.sourceKind),
2099
+ uri: row.sourceUri,
2100
+ externalId: row.sourceExternalId,
2101
+ title: row.sourceTitle,
2102
+ author: row.sourceAuthor,
2103
+ createdAt: row.sourceCreatedAt?.toISOString() ?? null,
2104
+ updatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
2105
+ version: row.sourceVersion
2106
+ },
2107
+ provenance: {
2108
+ ingestionWorkspaceId: row.workspaceId,
2109
+ baseId: row.baseId,
2110
+ fileId: row.fileId,
2111
+ authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
2112
+ authorityWorkspaceId: row.authorityWorkspaceId,
2113
+ authoritySubjectId: row.authoritySubjectId,
2114
+ createdBy: row.createdBy,
2115
+ createdAt: row.createdAt.toISOString()
2116
+ }
2117
+ };
2118
+ }
1511
2119
  function normalizeDocumentAuthorityKind(value) {
1512
2120
  switch (value) {
1513
2121
  case "organization":
@@ -1543,6 +2151,7 @@ export {
1543
2151
  DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES,
1544
2152
  DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE,
1545
2153
  DOCUMENT_CURATION_MAX_INPUT_CHARS,
2154
+ DOCUMENT_INDEX_CHECKPOINT_MAX_CHARS,
1546
2155
  DeterministicEmbeddingProvider,
1547
2156
  HeuristicCurationProvider,
1548
2157
  LiteParseDocumentParser,
@@ -1550,29 +2159,38 @@ export {
1550
2159
  OpenAIEmbeddingProvider,
1551
2160
  RecursiveTextChunker,
1552
2161
  addDocumentToBase,
2162
+ browseEffectiveKnowledge,
1553
2163
  canViewDocument,
1554
2164
  chunkText,
1555
2165
  createDocumentBase,
1556
2166
  createDocumentServices,
2167
+ decodeDocumentIndexCheckpoint,
2168
+ decodeKnowledgeBrowseCursor,
1557
2169
  deleteDocumentFromBase,
1558
2170
  deterministicEmbedding,
1559
2171
  documentOpenAIEmbeddingConfig,
2172
+ encodeDocumentIndexCheckpoint,
2173
+ encodeKnowledgeBrowseCursor,
1560
2174
  ensureDefaultBase,
1561
2175
  getDocument,
1562
2176
  getDocumentBase,
1563
2177
  getDocumentChunk,
1564
2178
  getDocumentInventory,
2179
+ getEffectiveKnowledgeRecord,
1565
2180
  heuristicCuration,
1566
2181
  indexDocumentNow,
1567
2182
  listDocumentBases,
1568
2183
  listDocumentBasesEnsuringDefault,
1569
2184
  listDocuments,
2185
+ listEffectiveIndexedDocuments,
1570
2186
  moveDocumentToBase,
1571
2187
  parseCurationOutcome,
1572
2188
  parseDocumentBytes,
2189
+ projectKnowledgeRecord,
1573
2190
  queueDocumentForReindex,
1574
2191
  resolveDocumentAuthority,
1575
2192
  searchDocuments,
1576
- searchEffectiveDocuments
2193
+ searchEffectiveDocuments,
2194
+ searchEffectiveKnowledge
1577
2195
  };
1578
2196
  //# sourceMappingURL=index.js.map