@opengeni/documents 0.5.39 → 0.6.9-canary.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.js CHANGED
@@ -1,6 +1,14 @@
1
1
  // src/index.ts
2
2
  import {
3
- requireFile,
3
+ KnowledgeProviderCitation,
4
+ DocumentAuthorityReclassification,
5
+ ListDocumentAuthorityReclassificationsResponse,
6
+ DocumentDefaultCollectionBackfill
7
+ } from "@opengeni/contracts";
8
+ import {
9
+ createPersonalDocumentAuthority,
10
+ getFilesForSubject,
11
+ resolveDocumentOriginalFileForSubject,
4
12
  rlsContextForWorkspace,
5
13
  setSubjectRlsContext,
6
14
  withRlsContext,
@@ -8,9 +16,20 @@ import {
8
16
  withWorkspaceSubjectRls
9
17
  } from "@opengeni/db";
10
18
  import * as schema from "@opengeni/db/schema";
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";
19
+ import { retryWhileMissing } from "@opengeni/storage";
20
+ import { createHash, randomUUID } from "crypto";
21
+ import { and, asc, desc, eq, gt, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
22
+ import {
23
+ KNOWLEDGE_BROWSE_CURSOR_MAX_CHARS,
24
+ KNOWLEDGE_BROWSE_MAX_LIMIT,
25
+ KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES,
26
+ KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES,
27
+ KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS,
28
+ KNOWLEDGE_SEARCH_MAX_RESULTS,
29
+ KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE,
30
+ KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
31
+ KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
32
+ } from "@opengeni/contracts";
14
33
 
15
34
  // src/knowledge-projection.ts
16
35
  import {
@@ -694,6 +713,135 @@ async function listDocumentBasesEnsuringDefault(db, input) {
694
713
  await ensureDefaultBase(db, input);
695
714
  return await listDocumentBases(db, input.workspaceId);
696
715
  }
716
+ async function runDocumentDefaultCollectionBackfill(db, input) {
717
+ return await withDocumentAccountRls(
718
+ db,
719
+ input.accountId,
720
+ input.workspaceId,
721
+ { viewerSubjectId: input.actorSubjectId },
722
+ async (scopedDb) => {
723
+ const command = {
724
+ accountId: input.accountId,
725
+ workspaceId: input.workspaceId,
726
+ actorSubjectId: input.actorSubjectId,
727
+ runId: input.runId,
728
+ operationId: input.operationId,
729
+ batchSize: input.batchSize,
730
+ accountAdminAuthorization: input.accountAdminAuthorization
731
+ };
732
+ const rows = await scopedDb.execute(sql`
733
+ SELECT run_document_default_collection_backfill(
734
+ ${JSON.stringify(command)}::jsonb
735
+ ) AS result
736
+ `);
737
+ const row = rows[0];
738
+ if (!row) throw new Error("Document Default collection backfill returned no result");
739
+ return DocumentDefaultCollectionBackfill.parse(row.result);
740
+ }
741
+ );
742
+ }
743
+ async function reclassifyDocumentAuthority(db, input) {
744
+ return await withDocumentAccountRls(
745
+ db,
746
+ input.accountId,
747
+ input.workspaceId,
748
+ { viewerSubjectId: input.actorSubjectId },
749
+ async (scopedDb) => {
750
+ const command = {
751
+ accountId: input.accountId,
752
+ workspaceId: input.workspaceId,
753
+ documentId: input.documentId,
754
+ operationId: input.operationId,
755
+ actorSubjectId: input.actorSubjectId,
756
+ expectedAuthority: input.expectedAuthority,
757
+ targetAuthorityKind: input.targetAuthorityKind,
758
+ accountAdminAuthorization: input.accountAdminAuthorization
759
+ };
760
+ const rows = await scopedDb.execute(sql`
761
+ SELECT reclassify_document_authority(${JSON.stringify(command)}::jsonb) AS result
762
+ `);
763
+ const row = rows[0];
764
+ if (!row) throw new Error("Document authority reclassification returned no result");
765
+ return DocumentAuthorityReclassification.parse(row.result);
766
+ }
767
+ );
768
+ }
769
+ async function listDocumentAuthorityReclassifications(db, input) {
770
+ const limit = input.limit ?? 50;
771
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
772
+ throw new Error("document authority receipt limit must be between 1 and 100");
773
+ }
774
+ const cursor = input.cursor ? decodeDocumentAuthorityReclassificationCursor(input.cursor, input) : null;
775
+ return await withDocumentAccountRls(
776
+ db,
777
+ input.accountId,
778
+ input.workspaceId,
779
+ { viewerSubjectId: input.actorSubjectId },
780
+ async (scopedDb) => {
781
+ const rows = await scopedDb.execute(sql`
782
+ SELECT list_document_authority_reclassifications(
783
+ ${input.accountId}::uuid,
784
+ ${input.workspaceId}::uuid,
785
+ ${input.actorSubjectId},
786
+ ${input.documentId}::uuid,
787
+ ${limit + 1}::integer,
788
+ ${cursor?.createdAt ?? null}::timestamptz,
789
+ ${cursor?.operationId ?? null}::uuid
790
+ ) AS result
791
+ `);
792
+ const parsed = rows.map((row) => DocumentAuthorityReclassification.parse(row.result));
793
+ const hasMore = parsed.length > limit;
794
+ const receipts = parsed.slice(0, limit);
795
+ const tail = hasMore ? receipts.at(-1) : null;
796
+ return ListDocumentAuthorityReclassificationsResponse.parse({
797
+ receipts,
798
+ hasMore,
799
+ nextCursor: tail ? encodeDocumentAuthorityReclassificationCursor(input, {
800
+ createdAt: tail.createdAt,
801
+ operationId: tail.operationId
802
+ }) : null
803
+ });
804
+ }
805
+ );
806
+ }
807
+ function documentAuthorityReclassificationCursorScope(input) {
808
+ return createHash("sha256").update(
809
+ JSON.stringify([
810
+ "document_authority_reclassification_cursor",
811
+ 1,
812
+ input.accountId,
813
+ input.workspaceId,
814
+ input.documentId,
815
+ input.actorSubjectId
816
+ ]),
817
+ "utf8"
818
+ ).digest("hex").slice(0, 32);
819
+ }
820
+ function encodeDocumentAuthorityReclassificationCursor(scope, cursor) {
821
+ return Buffer.from(
822
+ JSON.stringify({
823
+ v: 1,
824
+ s: documentAuthorityReclassificationCursorScope(scope),
825
+ t: cursor.createdAt,
826
+ i: cursor.operationId
827
+ }),
828
+ "utf8"
829
+ ).toString("base64url");
830
+ }
831
+ function decodeDocumentAuthorityReclassificationCursor(value, scope) {
832
+ try {
833
+ if (!value || value.length > 1024) throw new Error("cursor length");
834
+ const bytes = Buffer.from(value, "base64url");
835
+ if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
836
+ const parsed = JSON.parse(bytes.toString("utf8"));
837
+ if (Object.keys(parsed).sort().join(",") !== "i,s,t,v" || parsed.v !== 1 || parsed.s !== documentAuthorityReclassificationCursorScope(scope) || typeof parsed.t !== "string" || !Number.isFinite(new Date(parsed.t).getTime()) || typeof parsed.i !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(parsed.i)) {
838
+ throw new Error("cursor payload");
839
+ }
840
+ return { createdAt: parsed.t, operationId: parsed.i };
841
+ } catch (error) {
842
+ throw new Error("invalid document authority receipt cursor", { cause: error });
843
+ }
844
+ }
697
845
  async function addDocumentToBase(db, input) {
698
846
  return await withRlsContext(
699
847
  db,
@@ -715,7 +863,18 @@ async function addDocumentToBase(db, input) {
715
863
  if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
716
864
  const base = await getDocumentBase(scopedDb, input.workspaceId, input.baseId);
717
865
  if (!base) throw new Error(`Document base not found: ${input.baseId}`);
718
- const file = await requireReadyFile(scopedDb, input.workspaceId, input.fileId);
866
+ const initiatingSubjectId = cleanString(input.initiatingSubjectId ?? null);
867
+ const createdBy = cleanString(input.createdBy ?? null);
868
+ if (initiatingSubjectId && createdBy && initiatingSubjectId !== createdBy) {
869
+ throw new Error("document file authority must match the exact initiating subject");
870
+ }
871
+ const fileAuthoritySubjectId = initiatingSubjectId ?? createdBy ?? null;
872
+ const file = await requireReadyFile(scopedDb, {
873
+ accountId: input.accountId,
874
+ workspaceId: input.workspaceId,
875
+ subjectId: fileAuthoritySubjectId,
876
+ fileId: input.fileId
877
+ });
719
878
  const knowledgeSourceIdentity = cleanString(input.knowledgeSourceIdentity ?? null);
720
879
  if (knowledgeSourceIdentity && knowledgeSourceIdentity.length > 512) {
721
880
  throw new Error("knowledge source document identity exceeds 512 characters");
@@ -762,33 +921,47 @@ async function addDocumentToBase(db, input) {
762
921
  ).returning();
763
922
  return mapDocument(updated ?? existing);
764
923
  }
765
- const [row] = await scopedDb.insert(schema.documents).values({
766
- accountId: input.accountId,
767
- workspaceId: input.workspaceId,
768
- baseId: input.baseId,
769
- fileId: input.fileId,
770
- status: "queued",
771
- title: cleanString(input.title) ?? cleanString(input.sourceTitle) ?? file.filename,
772
- parser: DEFAULT_DOCUMENT_PARSER,
773
- sourceKind: input.sourceKind ?? "manual_upload",
774
- sourceUri: cleanString(input.sourceUri) ?? null,
775
- sourceExternalId: cleanString(input.sourceExternalId) ?? null,
776
- sourceTitle: cleanString(input.sourceTitle) ?? null,
777
- sourceAuthor: cleanString(input.sourceAuthor) ?? null,
778
- sourceCreatedAt: parseOptionalDate(input.sourceCreatedAt),
779
- sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
780
- sourceVersion: cleanString(input.sourceVersion) ?? null,
781
- knowledgeSourceIdentity,
782
- aclTags: cleanStringArray(input.aclTags),
783
- authorityKind: authority.kind,
784
- authorityWorkspaceId: authority.workspaceId,
785
- authoritySubjectId: authority.subjectId,
786
- visibility: authority.kind === "personal" ? "private" : "workspace",
787
- agentAccess: input.agentAccess ?? true,
788
- createdBy: input.createdBy ?? null,
789
- curationStatus: input.curationStatus ?? "none",
790
- updatedAt: now
791
- }).returning();
924
+ const documentId = randomUUID();
925
+ const row = await scopedDb.transaction(async (tx) => {
926
+ const userAuthority = authority.kind === "personal" ? await createPersonalDocumentAuthority(tx, {
927
+ accountId: input.accountId,
928
+ workspaceId: input.workspaceId,
929
+ subjectId: authority.subjectId,
930
+ documentId
931
+ }) : null;
932
+ const [inserted] = await tx.insert(schema.documents).values({
933
+ id: documentId,
934
+ accountId: input.accountId,
935
+ workspaceId: input.workspaceId,
936
+ baseId: input.baseId,
937
+ fileId: input.fileId,
938
+ status: "queued",
939
+ title: cleanString(input.title) ?? cleanString(input.sourceTitle) ?? file.filename,
940
+ parser: DEFAULT_DOCUMENT_PARSER,
941
+ sourceKind: input.sourceKind ?? "manual_upload",
942
+ sourceUri: cleanString(input.sourceUri) ?? null,
943
+ sourceExternalId: cleanString(input.sourceExternalId) ?? null,
944
+ sourceTitle: cleanString(input.sourceTitle) ?? null,
945
+ sourceAuthor: cleanString(input.sourceAuthor) ?? null,
946
+ sourceCreatedAt: parseOptionalDate(input.sourceCreatedAt),
947
+ sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
948
+ sourceVersion: cleanString(input.sourceVersion) ?? null,
949
+ knowledgeSourceIdentity,
950
+ aclTags: cleanStringArray(input.aclTags),
951
+ authorityKind: authority.kind,
952
+ authorityWorkspaceId: userAuthority ? null : authority.workspaceId,
953
+ authoritySubjectId: authority.subjectId,
954
+ authorityId: userAuthority?.authorityId ?? null,
955
+ ownerOrganizationMembershipId: userAuthority?.ownerOrganizationMembershipId ?? null,
956
+ originWorkspaceId: input.workspaceId,
957
+ visibility: authority.kind === "personal" ? "private" : "workspace",
958
+ agentAccess: input.agentAccess ?? true,
959
+ createdBy: fileAuthoritySubjectId,
960
+ curationStatus: input.curationStatus ?? "none",
961
+ updatedAt: now
962
+ }).returning();
963
+ return inserted;
964
+ });
792
965
  if (!row) throw new Error("Failed to create document");
793
966
  return mapDocument(row);
794
967
  }
@@ -803,7 +976,7 @@ async function moveDocumentToBase(db, input) {
803
976
  if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
804
977
  const [row] = await scopedDb.select().from(schema.documents).where(
805
978
  and(
806
- eq(schema.documents.workspaceId, input.workspaceId),
979
+ eq(schema.documents.accountId, input.accountId),
807
980
  eq(schema.documents.id, input.documentId),
808
981
  ...documentAccessConditions(input.workspaceId, input.access)
809
982
  )
@@ -816,11 +989,11 @@ async function moveDocumentToBase(db, input) {
816
989
  throw new Error("document has no suggested base; pass targetBaseId");
817
990
  }
818
991
  if (targetBaseId === row.baseId) return mapDocument(row);
819
- const base = await getDocumentBase(scopedDb, input.workspaceId, targetBaseId);
992
+ const base = await getDocumentBase(scopedDb, row.workspaceId, targetBaseId);
820
993
  if (!base) throw new Error(`Document base not found: ${targetBaseId}`);
821
994
  const [conflict] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
822
995
  and(
823
- eq(schema.documents.workspaceId, input.workspaceId),
996
+ eq(schema.documents.workspaceId, row.workspaceId),
824
997
  eq(schema.documents.baseId, targetBaseId),
825
998
  ...row.knowledgeSourceIdentity ? [eq(schema.documents.knowledgeSourceIdentity, row.knowledgeSourceIdentity)] : [eq(schema.documents.fileId, row.fileId)]
826
999
  )
@@ -836,7 +1009,7 @@ async function moveDocumentToBase(db, input) {
836
1009
  updatedAt: now
837
1010
  }).where(
838
1011
  and(
839
- eq(schema.documents.workspaceId, input.workspaceId),
1012
+ eq(schema.documents.workspaceId, row.workspaceId),
840
1013
  eq(schema.documents.id, input.documentId),
841
1014
  ...documentAccessConditions(input.workspaceId, input.access)
842
1015
  )
@@ -844,7 +1017,7 @@ async function moveDocumentToBase(db, input) {
844
1017
  if (updated) {
845
1018
  await tx.update(schema.documentChunks).set({ baseId: targetBaseId }).where(
846
1019
  and(
847
- eq(schema.documentChunks.workspaceId, input.workspaceId),
1020
+ eq(schema.documentChunks.workspaceId, row.workspaceId),
848
1021
  eq(schema.documentChunks.documentId, input.documentId)
849
1022
  )
850
1023
  );
@@ -865,7 +1038,7 @@ async function deleteDocumentFromBase(db, input) {
865
1038
  if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
866
1039
  const [document] = await scopedDb.select().from(schema.documents).where(
867
1040
  and(
868
- eq(schema.documents.workspaceId, input.workspaceId),
1041
+ eq(schema.documents.accountId, input.accountId),
869
1042
  eq(schema.documents.id, input.documentId),
870
1043
  ...documentAccessConditions(input.workspaceId, input.access)
871
1044
  )
@@ -882,7 +1055,7 @@ async function deleteDocumentFromBase(db, input) {
882
1055
  }
883
1056
  await scopedDb.delete(schema.documents).where(
884
1057
  and(
885
- eq(schema.documents.workspaceId, input.workspaceId),
1058
+ eq(schema.documents.accountId, input.accountId),
886
1059
  eq(schema.documents.id, input.documentId),
887
1060
  ...documentAccessConditions(input.workspaceId, input.access)
888
1061
  )
@@ -902,6 +1075,12 @@ async function listDocuments(db, workspaceId, baseId, access) {
902
1075
  return rows.map(mapDocument);
903
1076
  });
904
1077
  }
1078
+ async function listAccessibleDocuments(db, workspaceId, access) {
1079
+ return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1080
+ const rows = await scopedDb.select().from(schema.documents).where(and(...documentAccessConditions(workspaceId, access))).orderBy(desc(schema.documents.updatedAt), asc(schema.documents.createdAt));
1081
+ return rows.map(mapDocument);
1082
+ });
1083
+ }
905
1084
  async function listEffectiveIndexedDocuments(db, input) {
906
1085
  const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
907
1086
  const limit = input.limit ?? 50;
@@ -913,10 +1092,13 @@ async function listEffectiveIndexedDocuments(db, input) {
913
1092
  workspaceId: input.workspaceId,
914
1093
  initiatingSubjectId
915
1094
  }) : 0n;
916
- const access = {
917
- agentOnly: true,
918
- viewerSubjectId: initiatingSubjectId
919
- };
1095
+ const access = await resolveEffectiveDocumentAccess(db, {
1096
+ accountId: input.accountId,
1097
+ workspaceId: input.workspaceId,
1098
+ initiatingSubjectId,
1099
+ surface: "agent",
1100
+ agentAuthority: input.agentAuthority
1101
+ });
920
1102
  const rows = await withDocumentAccountRls(
921
1103
  db,
922
1104
  input.accountId,
@@ -1000,23 +1182,33 @@ function canonicalEffectiveDocumentSubject(value) {
1000
1182
  async function getDocument(db, workspaceId, documentId, access) {
1001
1183
  return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1002
1184
  const [row] = await scopedDb.select().from(schema.documents).where(
1003
- and(
1004
- eq(schema.documents.workspaceId, workspaceId),
1005
- eq(schema.documents.id, documentId),
1006
- ...documentAccessConditions(workspaceId, access)
1007
- )
1185
+ and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
1186
+ ).limit(1);
1187
+ return row ? mapDocument(row) : null;
1188
+ });
1189
+ }
1190
+ async function getDocumentOriginalFile(db, input) {
1191
+ const subjectId = cleanString(input.access.viewerSubjectId ?? null);
1192
+ if (!subjectId || input.access.agentOnly) return null;
1193
+ return await resolveDocumentOriginalFileForSubject(db, {
1194
+ accountId: input.accountId,
1195
+ workspaceId: input.workspaceId,
1196
+ subjectId,
1197
+ documentId: input.documentId
1198
+ });
1199
+ }
1200
+ async function getDocumentForIndexing(db, workspaceId, documentId) {
1201
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
1202
+ const [row] = await scopedDb.select().from(schema.documents).where(
1203
+ and(eq(schema.documents.workspaceId, workspaceId), eq(schema.documents.id, documentId))
1008
1204
  ).limit(1);
1009
1205
  return row ? mapDocument(row) : null;
1010
1206
  });
1011
1207
  }
1012
1208
  async function queueDocumentForReindex(db, workspaceId, documentId, access, organizationAuthorityGranted) {
1013
1209
  return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1014
- const [document] = await scopedDb.select({ authorityKind: schema.documents.authorityKind }).from(schema.documents).where(
1015
- and(
1016
- eq(schema.documents.workspaceId, workspaceId),
1017
- eq(schema.documents.id, documentId),
1018
- ...documentAccessConditions(workspaceId, access)
1019
- )
1210
+ const [document] = await scopedDb.select().from(schema.documents).where(
1211
+ and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
1020
1212
  ).limit(1);
1021
1213
  if (!document) throw new Error(`Document not found: ${documentId}`);
1022
1214
  assertOrganizationDocumentAuthority(document.authorityKind, organizationAuthorityGranted);
@@ -1025,11 +1217,7 @@ async function queueDocumentForReindex(db, workspaceId, documentId, access, orga
1025
1217
  error: null,
1026
1218
  updatedAt: /* @__PURE__ */ new Date()
1027
1219
  }).where(
1028
- and(
1029
- eq(schema.documents.workspaceId, workspaceId),
1030
- eq(schema.documents.id, documentId),
1031
- ...documentAccessConditions(workspaceId, access)
1032
- )
1220
+ and(eq(schema.documents.id, documentId), ...documentAccessConditions(workspaceId, access))
1033
1221
  ).returning();
1034
1222
  if (!row) throw new Error(`Document not found: ${documentId}`);
1035
1223
  return mapDocument(row);
@@ -1051,7 +1239,12 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
1051
1239
  );
1052
1240
  if (!loadedDocument) throw new Error(`Document not found: ${documentId}`);
1053
1241
  let document = loadedDocument;
1054
- const file = await requireReadyFile(db, workspaceId, document.fileId);
1242
+ const file = await requireReadyFile(db, {
1243
+ accountId: document.accountId,
1244
+ workspaceId,
1245
+ subjectId: cleanString(document.createdBy) ?? null,
1246
+ fileId: document.fileId
1247
+ });
1055
1248
  await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
1056
1249
  await scopedDb.update(schema.documents).set({
1057
1250
  status: "indexing",
@@ -1063,7 +1256,11 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
1063
1256
  );
1064
1257
  });
1065
1258
  try {
1066
- const bytes = await objectStorage.getFileBytes(file);
1259
+ const object = await retryWhileMissing(
1260
+ async () => objectStorage.getObjectBytes(file.objectKey)
1261
+ );
1262
+ if (!object) throw new Error("document source object is missing");
1263
+ const bytes = object.bytes;
1067
1264
  const parsed = await services.parser.parse(bytes, file);
1068
1265
  if (document.curationStatus === "pending") {
1069
1266
  document = await curateDroppedDocument(db, services, document, parsed, file);
@@ -1157,9 +1354,7 @@ async function indexDocumentNow(db, objectStorage, workspaceId, documentId, serv
1157
1354
  if (!failed) throw error;
1158
1355
  return mapDocument(failed);
1159
1356
  }
1160
- const updated = await getDocument(db, workspaceId, documentId, {
1161
- viewerSubjectId: document.authoritySubjectId
1162
- });
1357
+ const updated = await getDocumentForIndexing(db, workspaceId, documentId);
1163
1358
  if (!updated) throw new Error(`Document disappeared after indexing: ${documentId}`);
1164
1359
  return updated;
1165
1360
  }
@@ -1259,6 +1454,9 @@ async function curateDroppedDocument(db, services, document, parsed, file) {
1259
1454
  return updated ?? document;
1260
1455
  }
1261
1456
  async function searchDocuments(db, input, services = createDocumentServices()) {
1457
+ return (await searchDocumentCandidates(db, input, services)).results;
1458
+ }
1459
+ async function searchDocumentCandidates(db, input, services, relevanceFloor) {
1262
1460
  await assertDocumentAccountWorkspace(db, input.accountId, input.workspaceId);
1263
1461
  const mode = input.mode ?? "hybrid";
1264
1462
  const limit = Math.min(Math.max(input.limit ?? 5, 1), 50);
@@ -1283,13 +1481,34 @@ async function searchDocuments(db, input, services = createDocumentServices()) {
1283
1481
  if (mode === "keyword" || mode === "hybrid") {
1284
1482
  rows.push(...await keywordSearchDocuments(db, input, candidateLimit));
1285
1483
  }
1286
- return mergeDocumentSearchRows(rows, mode).slice(0, limit);
1484
+ const merged = mergeDocumentSearchRows(rows, mode);
1485
+ return selectDocumentSearchCandidateWindow(merged, limit, relevanceFloor);
1486
+ }
1487
+ function selectDocumentSearchCandidateWindow(merged, limit, relevanceFloor) {
1488
+ const boundedLimit = Math.min(Math.max(Math.trunc(limit), 1), 50);
1489
+ if (!relevanceFloor) {
1490
+ return { results: merged.slice(0, boundedLimit), belowRelevanceFloor: 0 };
1491
+ }
1492
+ let belowRelevanceFloor = 0;
1493
+ const relevant = merged.filter((result) => {
1494
+ const included = result.vectorScore !== null && result.vectorScore >= relevanceFloor.vectorScore || result.keywordScore !== null && result.keywordScore >= relevanceFloor.keywordScore;
1495
+ if (!included) belowRelevanceFloor += 1;
1496
+ return included;
1497
+ });
1498
+ return { results: relevant.slice(0, boundedLimit), belowRelevanceFloor };
1287
1499
  }
1288
1500
  async function searchEffectiveDocuments(db, input, services = createDocumentServices()) {
1289
1501
  const initiatingSubjectId = cleanString(input.initiatingSubjectId);
1290
1502
  if (!initiatingSubjectId) {
1291
1503
  throw new Error("effective document retrieval requires an initiating subject");
1292
1504
  }
1505
+ const access = await resolveEffectiveDocumentAccess(db, {
1506
+ accountId: input.accountId,
1507
+ workspaceId: input.workspaceId,
1508
+ initiatingSubjectId,
1509
+ surface: input.surface,
1510
+ agentAuthority: input.agentAuthority
1511
+ });
1293
1512
  return await searchDocuments(
1294
1513
  db,
1295
1514
  {
@@ -1303,29 +1522,62 @@ async function searchEffectiveDocuments(db, input, services = createDocumentServ
1303
1522
  aclTags: input.aclTags,
1304
1523
  // Construct the lower-level access filter here instead of spreading the
1305
1524
  // caller input, so an untyped/legacy access override is always ignored.
1306
- access: {
1307
- viewerSubjectId: initiatingSubjectId,
1308
- ...input.surface === "agent" ? { agentOnly: true } : {}
1309
- }
1525
+ access
1310
1526
  },
1311
1527
  services
1312
1528
  );
1313
1529
  }
1314
1530
  async function searchEffectiveKnowledge(db, input, services = createDocumentServices()) {
1315
1531
  const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
1316
- const ranked = await searchEffectiveDocuments(
1532
+ const requestedLimit = Math.min(Math.max(input.limit ?? 5, 1), KNOWLEDGE_SEARCH_MAX_RESULTS);
1533
+ const access = await resolveEffectiveDocumentAccess(db, {
1534
+ accountId: input.accountId,
1535
+ workspaceId: input.workspaceId,
1536
+ initiatingSubjectId,
1537
+ surface: input.surface,
1538
+ agentAuthority: input.agentAuthority
1539
+ });
1540
+ const candidateLimit = Math.min(requestedLimit * 4, KNOWLEDGE_SEARCH_MAX_RESULTS);
1541
+ const rankedSelection = await searchDocumentCandidates(
1317
1542
  db,
1318
- { ...input, initiatingSubjectId, surface: "agent" },
1319
- services
1543
+ {
1544
+ accountId: input.accountId,
1545
+ workspaceId: input.workspaceId,
1546
+ query: input.query,
1547
+ limit: candidateLimit,
1548
+ ...input.baseIds ? { baseIds: input.baseIds } : {},
1549
+ ...input.mode ? { mode: input.mode } : {},
1550
+ ...input.sourceKinds ? { sourceKinds: input.sourceKinds } : {},
1551
+ ...input.aclTags ? { aclTags: input.aclTags } : {},
1552
+ access
1553
+ },
1554
+ services,
1555
+ {
1556
+ vectorScore: KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
1557
+ keywordScore: KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE
1558
+ }
1320
1559
  );
1321
- if (ranked.length === 0) return { results: [] };
1322
- const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
1560
+ const ranked = rankedSelection.results;
1561
+ if (ranked.length === 0) {
1562
+ return selectKnowledgeSearchResults({
1563
+ candidates: [],
1564
+ rankedCandidateCount: 0,
1565
+ requestedLimit,
1566
+ alreadyBelowRelevanceFloor: rankedSelection.belowRelevanceFloor
1567
+ });
1568
+ }
1323
1569
  const current = await withDocumentAccountRls(
1324
1570
  db,
1325
1571
  input.accountId,
1326
1572
  input.workspaceId,
1327
1573
  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(
1574
+ async (scopedDb) => await scopedDb.select({
1575
+ chunk: schema.documentChunks,
1576
+ document: schema.documents,
1577
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1578
+ previousChunkId: knowledgePreviousChunkIdProjection(),
1579
+ nextChunkId: knowledgeNextChunkIdProjection()
1580
+ }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1329
1581
  and(
1330
1582
  eq(schema.documents.accountId, input.accountId),
1331
1583
  eq(schema.documentChunks.accountId, input.accountId),
@@ -1339,28 +1591,310 @@ async function searchEffectiveKnowledge(db, input, services = createDocumentServ
1339
1591
  )
1340
1592
  );
1341
1593
  const currentByChunkId = new Map(current.map((row) => [row.chunk.id, row]));
1342
- return {
1343
- results: ranked.flatMap((rankedResult) => {
1594
+ return selectKnowledgeSearchResults({
1595
+ rankedCandidateCount: ranked.length,
1596
+ requestedLimit,
1597
+ alreadyBelowRelevanceFloor: rankedSelection.belowRelevanceFloor,
1598
+ candidates: ranked.flatMap((rankedResult) => {
1344
1599
  const row = currentByChunkId.get(rankedResult.chunkId);
1345
1600
  if (!row) return [];
1346
1601
  return [
1347
1602
  {
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
- }
1603
+ record: knowledgeChunkRecord(row.document, row.chunk, row.citation, {
1604
+ previousChunkId: row.previousChunkId,
1605
+ nextChunkId: row.nextChunkId
1606
+ }),
1607
+ semanticScore: rankedResult.score,
1608
+ matchType: rankedResult.matchType,
1609
+ vectorScore: rankedResult.vectorScore,
1610
+ keywordScore: rankedResult.keywordScore
1355
1611
  }
1356
1612
  ];
1357
1613
  })
1614
+ });
1615
+ }
1616
+ function selectKnowledgeSearchResults(input) {
1617
+ const requestedLimit = Math.min(
1618
+ Math.max(Math.trunc(input.requestedLimit), 1),
1619
+ KNOWLEDGE_SEARCH_MAX_RESULTS
1620
+ );
1621
+ const nowMs = (input.now ?? /* @__PURE__ */ new Date()).getTime();
1622
+ let belowRelevanceFloor = Math.min(
1623
+ Math.max(0, Math.trunc(input.alreadyBelowRelevanceFloor ?? 0)),
1624
+ KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS
1625
+ );
1626
+ const relevant = [];
1627
+ for (const candidate of input.candidates) {
1628
+ const relevanceSignals = [];
1629
+ if (candidate.vectorScore !== null && candidate.vectorScore >= KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE) {
1630
+ relevanceSignals.push("vector");
1631
+ }
1632
+ if (candidate.keywordScore !== null && candidate.keywordScore >= KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE) {
1633
+ relevanceSignals.push("keyword");
1634
+ }
1635
+ if (relevanceSignals.length === 0) {
1636
+ belowRelevanceFloor = Math.min(belowRelevanceFloor + 1, KNOWLEDGE_SEARCH_MAX_FLOOR_OMISSIONS);
1637
+ continue;
1638
+ }
1639
+ const freshness = knowledgeFreshness(candidate.record.quality.freshnessAt, nowMs);
1640
+ const qualityAdjustment = freshness === "current" ? 0.02 : freshness === "aging" ? 0.01 : 0;
1641
+ relevant.push({
1642
+ record: candidate.record,
1643
+ retrieval: {
1644
+ score: roundScore(Math.min(1, candidate.semanticScore + qualityAdjustment)),
1645
+ semanticScore: roundScore(candidate.semanticScore),
1646
+ matchType: candidate.matchType,
1647
+ vectorScore: candidate.vectorScore === null ? null : roundScore(candidate.vectorScore),
1648
+ keywordScore: candidate.keywordScore === null ? null : roundScore(candidate.keywordScore),
1649
+ relevanceSignals,
1650
+ freshness,
1651
+ qualityAdjustment,
1652
+ duplicateCount: 0
1653
+ }
1654
+ });
1655
+ }
1656
+ relevant.sort(compareKnowledgeSearchResults);
1657
+ const deduped = [];
1658
+ const byContent = /* @__PURE__ */ new Map();
1659
+ let asDuplicate = 0;
1660
+ for (const result of relevant) {
1661
+ const key = knowledgeTextualContentKey(result.record);
1662
+ const retainedIndex = byContent.get(key);
1663
+ if (retainedIndex === void 0) {
1664
+ byContent.set(key, deduped.length);
1665
+ deduped.push(result);
1666
+ continue;
1667
+ }
1668
+ asDuplicate += 1;
1669
+ const retained = deduped[retainedIndex];
1670
+ retained.retrieval.duplicateCount += 1;
1671
+ }
1672
+ const forLimit = Math.max(0, deduped.length - requestedLimit);
1673
+ const bounded = deduped.slice(0, requestedLimit);
1674
+ let forResponseBudget = 0;
1675
+ let response = knowledgeSearchResponse({
1676
+ results: bounded,
1677
+ rankedCandidateCount: input.rankedCandidateCount,
1678
+ recheckedCandidateCount: input.candidates.length,
1679
+ belowRelevanceFloor,
1680
+ asDuplicate,
1681
+ forLimit,
1682
+ forResponseBudget
1683
+ });
1684
+ while (knowledgeResponseBytes(response) > KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES) {
1685
+ if (bounded.length === 0) {
1686
+ throw new Error("knowledge search selection facts exceed the response budget");
1687
+ }
1688
+ bounded.pop();
1689
+ forResponseBudget += 1;
1690
+ response = knowledgeSearchResponse({
1691
+ results: bounded,
1692
+ rankedCandidateCount: input.rankedCandidateCount,
1693
+ recheckedCandidateCount: input.candidates.length,
1694
+ belowRelevanceFloor,
1695
+ asDuplicate,
1696
+ forLimit,
1697
+ forResponseBudget
1698
+ });
1699
+ }
1700
+ return response;
1701
+ }
1702
+ function compareKnowledgeSearchResults(left, right) {
1703
+ return right.retrieval.score - left.retrieval.score || right.retrieval.semanticScore - left.retrieval.semanticScore || (right.retrieval.vectorScore ?? 0) - (left.retrieval.vectorScore ?? 0) || (right.retrieval.keywordScore ?? 0) - (left.retrieval.keywordScore ?? 0) || (left.record.id === right.record.id ? 0 : left.record.id < right.record.id ? -1 : 1);
1704
+ }
1705
+ function knowledgeFreshness(value, nowMs) {
1706
+ const freshnessMs = Date.parse(value);
1707
+ if (!Number.isFinite(freshnessMs)) return "stale";
1708
+ const ageDays = Math.max(0, (nowMs - freshnessMs) / 864e5);
1709
+ if (ageDays <= 90) return "current";
1710
+ return ageDays <= 365 ? "aging" : "stale";
1711
+ }
1712
+ function knowledgeTextualContentKey(record) {
1713
+ return createHash("sha256").update("opengeni:knowledge-search-content:v1\0").update(record.title).update("\0").update(
1714
+ JSON.stringify({
1715
+ body: record.content.body,
1716
+ summary: record.content.summary,
1717
+ topics: record.content.topics
1718
+ })
1719
+ ).digest("hex");
1720
+ }
1721
+ function knowledgeSearchResponse(input) {
1722
+ const selection = {
1723
+ relevanceFloor: {
1724
+ policy: "any_signal",
1725
+ vectorScore: KNOWLEDGE_SEARCH_MIN_VECTOR_SCORE,
1726
+ keywordScore: KNOWLEDGE_SEARCH_MIN_KEYWORD_SCORE
1727
+ },
1728
+ dedupe: { policy: "exact_textual_content" },
1729
+ candidates: {
1730
+ ranked: input.rankedCandidateCount,
1731
+ rechecked: input.recheckedCandidateCount,
1732
+ omittedOnRecheck: Math.max(0, input.rankedCandidateCount - input.recheckedCandidateCount)
1733
+ },
1734
+ omitted: {
1735
+ belowRelevanceFloor: input.belowRelevanceFloor,
1736
+ asDuplicate: input.asDuplicate,
1737
+ forLimit: input.forLimit,
1738
+ forResponseBudget: input.forResponseBudget
1739
+ },
1740
+ budget: {
1741
+ maxResults: KNOWLEDGE_SEARCH_MAX_RESULTS,
1742
+ maxResponseBytes: KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES,
1743
+ responseBytes: 0,
1744
+ tokenEstimateBytesPerToken: KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN,
1745
+ estimatedTokens: 0,
1746
+ maxEstimatedTokens: Math.ceil(
1747
+ KNOWLEDGE_SEARCH_MAX_RESPONSE_BYTES / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1748
+ )
1749
+ }
1750
+ };
1751
+ const response = {
1752
+ results: [...input.results],
1753
+ selection
1754
+ };
1755
+ for (let index = 0; index < 8; index += 1) {
1756
+ const responseBytes = knowledgeResponseBytes(response);
1757
+ const estimatedTokens = Math.ceil(
1758
+ responseBytes / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1759
+ );
1760
+ if (response.selection.budget.responseBytes === responseBytes && response.selection.budget.estimatedTokens === estimatedTokens) {
1761
+ break;
1762
+ }
1763
+ response.selection.budget.responseBytes = responseBytes;
1764
+ response.selection.budget.estimatedTokens = estimatedTokens;
1765
+ }
1766
+ return response;
1767
+ }
1768
+ function knowledgeResponseBytes(response) {
1769
+ return Buffer.byteLength(JSON.stringify(response), "utf8");
1770
+ }
1771
+ function selectKnowledgeBrowseRecords(input) {
1772
+ const entries = input.entries.slice(0, KNOWLEDGE_BROWSE_MAX_LIMIT);
1773
+ const selected = entries.map((entry) => entry.record);
1774
+ let omittedForResponseBudget = 0;
1775
+ let compactedRecordCount = 0;
1776
+ let response = knowledgeBrowseResponse({
1777
+ records: selected,
1778
+ nextCursor: input.hasMoreAfterEntries ? entries.at(-1)?.cursorAfter ?? null : null,
1779
+ hasMore: input.hasMoreAfterEntries,
1780
+ omittedForResponseBudget,
1781
+ compactedRecordCount
1782
+ });
1783
+ while (selected.length > 1 && knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
1784
+ selected.pop();
1785
+ omittedForResponseBudget += 1;
1786
+ response = knowledgeBrowseResponse({
1787
+ records: selected,
1788
+ nextCursor: entries[selected.length - 1]?.cursorAfter ?? null,
1789
+ hasMore: true,
1790
+ omittedForResponseBudget,
1791
+ compactedRecordCount
1792
+ });
1793
+ }
1794
+ if (selected.length === 1 && knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
1795
+ selected[0] = compactKnowledgeBrowseRecord(selected[0]);
1796
+ compactedRecordCount = 1;
1797
+ response = knowledgeBrowseResponse({
1798
+ records: selected,
1799
+ nextCursor: omittedForResponseBudget > 0 || input.hasMoreAfterEntries ? entries[0]?.cursorAfter ?? null : null,
1800
+ hasMore: omittedForResponseBudget > 0 || input.hasMoreAfterEntries,
1801
+ omittedForResponseBudget,
1802
+ compactedRecordCount
1803
+ });
1804
+ }
1805
+ if (knowledgeBrowseResponseBytes(response) > KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES) {
1806
+ throw new Error("knowledge browse discovery projection exceeds the response budget");
1807
+ }
1808
+ return response;
1809
+ }
1810
+ function compactKnowledgeBrowseRecord(record) {
1811
+ const fields = /* @__PURE__ */ new Set([
1812
+ ...record.projection.fields,
1813
+ "content.body",
1814
+ "content.summary",
1815
+ "content.topics",
1816
+ "content.metadata",
1817
+ "provenance.source.uri",
1818
+ "provenance.source.externalId",
1819
+ "provenance.source.title",
1820
+ "provenance.source.author",
1821
+ "provenance.source.version",
1822
+ "provenance.citation"
1823
+ ]);
1824
+ return {
1825
+ ...record,
1826
+ content: {
1827
+ format: "markdown",
1828
+ body: null,
1829
+ summary: null,
1830
+ topics: [],
1831
+ metadata: {}
1832
+ },
1833
+ provenance: {
1834
+ ...record.provenance,
1835
+ source: {
1836
+ ...record.provenance.source,
1837
+ uri: null,
1838
+ externalId: null,
1839
+ title: null,
1840
+ author: null,
1841
+ version: null
1842
+ },
1843
+ citation: null
1844
+ },
1845
+ links: record.links.filter((link) => link.target.kind === "knowledge"),
1846
+ projection: {
1847
+ truncated: true,
1848
+ fields: [...fields].sort()
1849
+ }
1358
1850
  };
1359
1851
  }
1852
+ function knowledgeBrowseResponse(input) {
1853
+ const response = {
1854
+ records: [...input.records],
1855
+ nextCursor: input.nextCursor,
1856
+ hasMore: input.hasMore,
1857
+ selection: {
1858
+ omitted: { forResponseBudget: input.omittedForResponseBudget },
1859
+ compactedRecordCount: input.compactedRecordCount,
1860
+ budget: {
1861
+ maxResults: KNOWLEDGE_BROWSE_MAX_LIMIT,
1862
+ maxResponseBytes: KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES,
1863
+ responseBytes: 0,
1864
+ tokenEstimateBytesPerToken: KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN,
1865
+ estimatedTokens: 0,
1866
+ maxEstimatedTokens: Math.ceil(
1867
+ KNOWLEDGE_BROWSE_MAX_RESPONSE_BYTES / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1868
+ )
1869
+ }
1870
+ }
1871
+ };
1872
+ for (let index = 0; index < 8; index += 1) {
1873
+ const responseBytes = knowledgeBrowseResponseBytes(response);
1874
+ const estimatedTokens = Math.ceil(
1875
+ responseBytes / KNOWLEDGE_SEARCH_TOKEN_ESTIMATE_BYTES_PER_TOKEN
1876
+ );
1877
+ if (response.selection.budget.responseBytes === responseBytes && response.selection.budget.estimatedTokens === estimatedTokens) {
1878
+ break;
1879
+ }
1880
+ response.selection.budget.responseBytes = responseBytes;
1881
+ response.selection.budget.estimatedTokens = estimatedTokens;
1882
+ }
1883
+ return response;
1884
+ }
1885
+ function knowledgeBrowseResponseBytes(response) {
1886
+ return Buffer.byteLength(JSON.stringify(response), "utf8");
1887
+ }
1360
1888
  async function getEffectiveKnowledgeRecord(db, input) {
1361
1889
  const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
1362
1890
  const target = parseKnowledgeRecordId(input.id);
1363
- const access = { agentOnly: true, viewerSubjectId: initiatingSubjectId };
1891
+ const access = await resolveEffectiveDocumentAccess(db, {
1892
+ accountId: input.accountId,
1893
+ workspaceId: input.workspaceId,
1894
+ initiatingSubjectId,
1895
+ surface: input.surface ?? "agent",
1896
+ agentAuthority: input.agentAuthority
1897
+ });
1364
1898
  return await withDocumentAccountRls(
1365
1899
  db,
1366
1900
  input.accountId,
@@ -1368,7 +1902,18 @@ async function getEffectiveKnowledgeRecord(db, input) {
1368
1902
  access,
1369
1903
  async (scopedDb) => {
1370
1904
  if (target.kind === "document") {
1371
- const [document] = await scopedDb.select().from(schema.documents).where(
1905
+ const [row2] = await scopedDb.select({
1906
+ document: schema.documents,
1907
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1908
+ firstChunkId: schema.documentChunks.id
1909
+ }).from(schema.documents).leftJoin(
1910
+ schema.documentChunks,
1911
+ and(
1912
+ eq(schema.documentChunks.accountId, schema.documents.accountId),
1913
+ eq(schema.documentChunks.documentId, schema.documents.id),
1914
+ eq(schema.documentChunks.chunkIndex, 0)
1915
+ )
1916
+ ).where(
1372
1917
  and(
1373
1918
  eq(schema.documents.accountId, input.accountId),
1374
1919
  eq(schema.documents.id, target.id),
@@ -1376,9 +1921,15 @@ async function getEffectiveKnowledgeRecord(db, input) {
1376
1921
  ...documentAccessConditions(input.workspaceId, access)
1377
1922
  )
1378
1923
  ).limit(1);
1379
- return document ? knowledgeDocumentRecord(document) : null;
1924
+ return row2 ? knowledgeDocumentRecord(row2.document, row2.citation, row2.firstChunkId) : null;
1380
1925
  }
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(
1926
+ const [row] = await scopedDb.select({
1927
+ chunk: schema.documentChunks,
1928
+ document: schema.documents,
1929
+ citation: googleDriveCitationProjection(input.workspaceId, access),
1930
+ previousChunkId: knowledgePreviousChunkIdProjection(),
1931
+ nextChunkId: knowledgeNextChunkIdProjection()
1932
+ }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1382
1933
  and(
1383
1934
  eq(schema.documents.accountId, input.accountId),
1384
1935
  eq(schema.documentChunks.accountId, input.accountId),
@@ -1387,7 +1938,10 @@ async function getEffectiveKnowledgeRecord(db, input) {
1387
1938
  ...documentAccessConditions(input.workspaceId, access)
1388
1939
  )
1389
1940
  ).limit(1);
1390
- return row ? knowledgeChunkRecord(row.document, row.chunk) : null;
1941
+ return row ? knowledgeChunkRecord(row.document, row.chunk, row.citation, {
1942
+ previousChunkId: row.previousChunkId,
1943
+ nextChunkId: row.nextChunkId
1944
+ }) : null;
1391
1945
  }
1392
1946
  );
1393
1947
  }
@@ -1417,11 +1971,14 @@ async function browseEffectiveKnowledge(db, input) {
1417
1971
  topic,
1418
1972
  sourceKinds
1419
1973
  };
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 };
1974
+ const topLevelAfter = !parent && input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, cursorScope) : 0n;
1975
+ const access = await resolveEffectiveDocumentAccess(db, {
1976
+ accountId: input.accountId,
1977
+ workspaceId: input.workspaceId,
1978
+ initiatingSubjectId,
1979
+ surface: input.surface ?? "agent",
1980
+ agentAuthority: input.agentAuthority
1981
+ });
1425
1982
  return await withDocumentAccountRls(
1426
1983
  db,
1427
1984
  input.accountId,
@@ -1429,7 +1986,7 @@ async function browseEffectiveKnowledge(db, input) {
1429
1986
  access,
1430
1987
  async (scopedDb) => {
1431
1988
  if (parent) {
1432
- const [authorizedParent] = await scopedDb.select({ id: schema.documents.id }).from(schema.documents).where(
1989
+ const [authorizedParent] = await scopedDb.select({ id: schema.documents.id, indexSequence: schema.documents.indexSequence }).from(schema.documents).where(
1433
1990
  and(
1434
1991
  eq(schema.documents.accountId, input.accountId),
1435
1992
  eq(schema.documents.id, parent.id),
@@ -1437,51 +1994,96 @@ async function browseEffectiveKnowledge(db, input) {
1437
1994
  ...documentAccessConditions(input.workspaceId, access)
1438
1995
  )
1439
1996
  ).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(
1997
+ if (!authorizedParent) {
1998
+ return selectKnowledgeBrowseRecords({ entries: [], hasMoreAfterEntries: false });
1999
+ }
2000
+ if (authorizedParent.indexSequence === null) {
2001
+ throw new Error("ready knowledge document is missing its index revision");
2002
+ }
2003
+ const parentCursorScope = {
2004
+ ...cursorScope,
2005
+ parentRevision: authorizedParent.indexSequence.toString()
2006
+ };
2007
+ const after = input.cursor ? decodeKnowledgeBrowseCursor(input.cursor, parentCursorScope) : 0n;
2008
+ if (after > 2147483648n) {
2009
+ throw new Error("invalid knowledge browse cursor");
2010
+ }
2011
+ const rows2 = await scopedDb.select({
2012
+ chunk: schema.documentChunks,
2013
+ document: schema.documents,
2014
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2015
+ previousChunkId: knowledgePreviousChunkIdProjection(),
2016
+ nextChunkId: knowledgeNextChunkIdProjection()
2017
+ }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1442
2018
  and(
1443
2019
  eq(schema.documentChunks.accountId, input.accountId),
1444
2020
  eq(schema.documentChunks.documentId, parent.id),
1445
2021
  gt(schema.documentChunks.chunkIndex, Number(after) - 1),
1446
2022
  eq(schema.documents.status, "ready"),
2023
+ eq(schema.documents.indexSequence, authorizedParent.indexSequence),
1447
2024
  ...documentAccessConditions(input.workspaceId, access)
1448
2025
  )
1449
2026
  ).orderBy(asc(schema.documentChunks.chunkIndex)).limit(limit + 1);
1450
2027
  const hasMore2 = rows2.length > limit;
1451
2028
  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
- };
2029
+ return selectKnowledgeBrowseRecords({
2030
+ entries: page2.map((row) => ({
2031
+ record: knowledgeChunkRecord(row.document, row.chunk, row.citation, {
2032
+ previousChunkId: row.previousChunkId,
2033
+ nextChunkId: row.nextChunkId
2034
+ }),
2035
+ cursorAfter: encodeKnowledgeBrowseCursor(
2036
+ parentCursorScope,
2037
+ BigInt(row.chunk.chunkIndex + 1)
2038
+ )
2039
+ })),
2040
+ hasMoreAfterEntries: hasMore2
2041
+ });
1458
2042
  }
1459
2043
  const conditions = [
1460
2044
  eq(schema.documents.accountId, input.accountId),
1461
2045
  eq(schema.documents.status, "ready"),
1462
2046
  isNotNull(schema.documents.indexSequence),
1463
- gt(schema.documents.indexSequence, after),
2047
+ gt(schema.documents.indexSequence, topLevelAfter),
1464
2048
  ...documentAccessConditions(input.workspaceId, access)
1465
2049
  ];
1466
2050
  if (topic) conditions.push(sql`${schema.documents.topics} ? ${topic}`);
1467
2051
  if (sourceKinds.length > 0)
1468
2052
  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);
2053
+ const rows = await scopedDb.select({
2054
+ document: schema.documents,
2055
+ citation: googleDriveCitationProjection(input.workspaceId, access),
2056
+ firstChunkId: schema.documentChunks.id
2057
+ }).from(schema.documents).leftJoin(
2058
+ schema.documentChunks,
2059
+ and(
2060
+ eq(schema.documentChunks.accountId, schema.documents.accountId),
2061
+ eq(schema.documentChunks.documentId, schema.documents.id),
2062
+ eq(schema.documentChunks.chunkIndex, 0)
2063
+ )
2064
+ ).where(and(...conditions)).orderBy(asc(schema.documents.indexSequence)).limit(limit + 1);
1470
2065
  const hasMore = rows.length > limit;
1471
2066
  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
- };
2067
+ return selectKnowledgeBrowseRecords({
2068
+ entries: page.map((row) => {
2069
+ if (row.document.indexSequence === null) {
2070
+ throw new Error("ready knowledge document is missing its index revision");
2071
+ }
2072
+ return {
2073
+ record: knowledgeDocumentRecord(row.document, row.citation, row.firstChunkId),
2074
+ cursorAfter: encodeKnowledgeBrowseCursor(cursorScope, row.document.indexSequence)
2075
+ };
2076
+ }),
2077
+ hasMoreAfterEntries: hasMore
2078
+ });
1478
2079
  }
1479
2080
  );
1480
2081
  }
1481
2082
  function encodeKnowledgeBrowseCursor(scope, position) {
1482
2083
  if (position < 0n) throw new Error("knowledge browse cursor position is invalid");
2084
+ const version = knowledgeBrowseCursorVersion(scope);
1483
2085
  return Buffer.from(
1484
- JSON.stringify({ v: 1, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
2086
+ JSON.stringify({ v: version, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
1485
2087
  "utf8"
1486
2088
  ).toString("base64url");
1487
2089
  }
@@ -1493,7 +2095,8 @@ function decodeKnowledgeBrowseCursor(value, scope) {
1493
2095
  const bytes = Buffer.from(value, "base64url");
1494
2096
  if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
1495
2097
  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)) {
2098
+ const expectedVersion = knowledgeBrowseCursorVersion(scope);
2099
+ if (Object.keys(parsed).sort().join(",") !== "q,s,v" || parsed.v !== expectedVersion || typeof parsed.s !== "string" || typeof parsed.q !== "string" || !/^(0|[1-9][0-9]*)$/.test(parsed.q)) {
1497
2100
  throw new Error("cursor payload");
1498
2101
  }
1499
2102
  if (parsed.s !== knowledgeBrowseCursorScope(scope)) {
@@ -1510,7 +2113,17 @@ function decodeKnowledgeBrowseCursor(value, scope) {
1510
2113
  }
1511
2114
  }
1512
2115
  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");
2116
+ const version = knowledgeBrowseCursorVersion(scope);
2117
+ const hash = createHash("sha256").update(`opengeni:knowledge-browse-cursor:v${version}\0`).update(scope.accountId).update("\0").update(scope.workspaceId).update("\0").update(canonicalEffectiveDocumentSubject(scope.initiatingSubjectId)).update("\0").update(scope.parentId ?? "").update("\0");
2118
+ if (version === 2) hash.update(scope.parentRevision).update("\0");
2119
+ return hash.update(scope.topic ?? "").update("\0").update([...scope.sourceKinds].sort().join("\0")).digest("hex");
2120
+ }
2121
+ function knowledgeBrowseCursorVersion(scope) {
2122
+ if (!scope.parentId) return 1;
2123
+ if (!scope.parentRevision || !/^[1-9][0-9]*$/.test(scope.parentRevision)) {
2124
+ throw new Error("knowledge browse parent cursor requires an exact document revision");
2125
+ }
2126
+ return 2;
1514
2127
  }
1515
2128
  function parseKnowledgeRecordId(value) {
1516
2129
  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(
@@ -1519,7 +2132,7 @@ function parseKnowledgeRecordId(value) {
1519
2132
  if (!match) throw new Error("invalid knowledge record id");
1520
2133
  return { kind: match[1], id: match[2].toLowerCase() };
1521
2134
  }
1522
- function knowledgeDocumentRecord(document) {
2135
+ function knowledgeDocumentRecord(document, citation = null, firstChunkId = null) {
1523
2136
  if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
1524
2137
  const projected = projectKnowledgeRecord({
1525
2138
  title: document.title,
@@ -1537,15 +2150,27 @@ function knowledgeDocumentRecord(document) {
1537
2150
  authority: { kind: normalizeDocumentAuthorityKind(document.authorityKind) },
1538
2151
  provenance: {
1539
2152
  source: projected.source,
1540
- indexedAt: document.indexedAt.toISOString()
2153
+ indexedAt: document.indexedAt.toISOString(),
2154
+ citation: parseKnowledgeProviderCitation(citation)
1541
2155
  },
1542
2156
  lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
1543
2157
  quality: knowledgeQuality(document),
1544
- links: knowledgeSourceLinks(projected.source.uri),
2158
+ links: [
2159
+ ...firstChunkId ? [
2160
+ {
2161
+ relation: "contents",
2162
+ target: {
2163
+ kind: "knowledge",
2164
+ id: `document_chunk:${firstChunkId}`
2165
+ }
2166
+ }
2167
+ ] : [],
2168
+ ...knowledgeSourceLinks(projected.source.uri)
2169
+ ],
1545
2170
  projection: projected.projection
1546
2171
  };
1547
2172
  }
1548
- function knowledgeChunkRecord(document, chunk) {
2173
+ function knowledgeChunkRecord(document, chunk, citation = null, traversal = { previousChunkId: null, nextChunkId: null }) {
1549
2174
  if (!document.indexedAt) throw new Error(`Ready document is missing indexed_at: ${document.id}`);
1550
2175
  const projected = projectKnowledgeRecord({
1551
2176
  title: document.title,
@@ -1563,12 +2188,34 @@ function knowledgeChunkRecord(document, chunk) {
1563
2188
  authority: { kind: normalizeDocumentAuthorityKind(document.authorityKind) },
1564
2189
  provenance: {
1565
2190
  source: projected.source,
1566
- indexedAt: document.indexedAt.toISOString()
2191
+ indexedAt: document.indexedAt.toISOString(),
2192
+ citation: parseKnowledgeProviderCitation(citation)
1567
2193
  },
1568
2194
  lifecycle: { state: "active", updatedAt: document.updatedAt.toISOString() },
1569
2195
  quality: knowledgeQuality(document),
1570
2196
  links: [
1571
- { relation: "parent", target: { kind: "knowledge", id: `document:${document.id}` } },
2197
+ {
2198
+ relation: "parent",
2199
+ target: { kind: "knowledge", id: `document:${document.id}` }
2200
+ },
2201
+ ...traversal.previousChunkId ? [
2202
+ {
2203
+ relation: "previous",
2204
+ target: {
2205
+ kind: "knowledge",
2206
+ id: `document_chunk:${traversal.previousChunkId}`
2207
+ }
2208
+ }
2209
+ ] : [],
2210
+ ...traversal.nextChunkId ? [
2211
+ {
2212
+ relation: "next",
2213
+ target: {
2214
+ kind: "knowledge",
2215
+ id: `document_chunk:${traversal.nextChunkId}`
2216
+ }
2217
+ }
2218
+ ] : [],
1572
2219
  ...knowledgeSourceLinks(projected.source.uri)
1573
2220
  ],
1574
2221
  projection: projected.projection
@@ -1598,6 +2245,28 @@ function knowledgeQuality(document) {
1598
2245
  function knowledgeSourceLinks(sourceUri) {
1599
2246
  return sourceUri ? [{ relation: "source", target: { kind: "external", uri: sourceUri } }] : [];
1600
2247
  }
2248
+ function knowledgePreviousChunkIdProjection() {
2249
+ return sql`(
2250
+ select knowledge_previous_chunk.id
2251
+ from document_chunks knowledge_previous_chunk
2252
+ where knowledge_previous_chunk.account_id = ${schema.documentChunks.accountId}
2253
+ and knowledge_previous_chunk.document_id = ${schema.documentChunks.documentId}
2254
+ and knowledge_previous_chunk.chunk_index < ${schema.documentChunks.chunkIndex}
2255
+ order by knowledge_previous_chunk.chunk_index desc
2256
+ limit 1
2257
+ )`;
2258
+ }
2259
+ function knowledgeNextChunkIdProjection() {
2260
+ return sql`(
2261
+ select knowledge_next_chunk.id
2262
+ from document_chunks knowledge_next_chunk
2263
+ where knowledge_next_chunk.account_id = ${schema.documentChunks.accountId}
2264
+ and knowledge_next_chunk.document_id = ${schema.documentChunks.documentId}
2265
+ and knowledge_next_chunk.chunk_index > ${schema.documentChunks.chunkIndex}
2266
+ order by knowledge_next_chunk.chunk_index asc
2267
+ limit 1
2268
+ )`;
2269
+ }
1601
2270
  async function vectorSearchDocuments(db, input, limit, services) {
1602
2271
  const queryEmbedding = await services.embedder.embedQuery(input.query);
1603
2272
  validateEmbedding(queryEmbedding, services.embedder.dimensions, services.embedder.model);
@@ -1629,8 +2298,9 @@ async function vectorSearchDocuments(db, input, limit, services) {
1629
2298
  authorityKind: schema.documents.authorityKind,
1630
2299
  authorityWorkspaceId: schema.documents.authorityWorkspaceId,
1631
2300
  authoritySubjectId: schema.documents.authoritySubjectId,
2301
+ citation: googleDriveCitationProjection(input.workspaceId, input.access),
1632
2302
  distance
1633
- }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(and(...documentSearchConditions(input, services.embedder.model))).orderBy(distance).limit(limit)
2303
+ }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(and(...documentSearchConditions(input, services.embedder.model))).orderBy(distance, asc(schema.documentChunks.id)).limit(limit)
1634
2304
  );
1635
2305
  return rows.map((row) => ({
1636
2306
  ...mapSearchRowBase(row),
@@ -1667,13 +2337,14 @@ async function keywordSearchDocuments(db, input, limit) {
1667
2337
  authorityKind: schema.documents.authorityKind,
1668
2338
  authorityWorkspaceId: schema.documents.authorityWorkspaceId,
1669
2339
  authoritySubjectId: schema.documents.authoritySubjectId,
2340
+ citation: googleDriveCitationProjection(input.workspaceId, input.access),
1670
2341
  rank
1671
2342
  }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1672
2343
  and(
1673
2344
  ...documentSearchConditions(input),
1674
2345
  sql`to_tsvector('simple', ${schema.documentChunks.text}) @@ plainto_tsquery('simple', ${input.query})`
1675
2346
  )
1676
- ).orderBy(desc(rank)).limit(limit)
2347
+ ).orderBy(desc(rank), asc(schema.documentChunks.id)).limit(limit)
1677
2348
  );
1678
2349
  return rows.map((row) => ({
1679
2350
  ...mapSearchRowBase(row),
@@ -1708,7 +2379,8 @@ async function getDocumentChunk(db, accountId, workspaceId, chunkId, access) {
1708
2379
  aclTags: schema.documents.aclTags,
1709
2380
  authorityKind: schema.documents.authorityKind,
1710
2381
  authorityWorkspaceId: schema.documents.authorityWorkspaceId,
1711
- authoritySubjectId: schema.documents.authoritySubjectId
2382
+ authoritySubjectId: schema.documents.authoritySubjectId,
2383
+ citation: googleDriveCitationProjection(workspaceId, access)
1712
2384
  }).from(schema.documentChunks).innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id)).where(
1713
2385
  and(
1714
2386
  eq(schema.documents.accountId, accountId),
@@ -1768,6 +2440,21 @@ async function assertDocumentAccountWorkspace(db, accountId, workspaceId) {
1768
2440
  }
1769
2441
  return context;
1770
2442
  }
2443
+ async function resolveEffectiveDocumentAccess(_db, input) {
2444
+ if (input.surface === "human") {
2445
+ return { viewerSubjectId: input.initiatingSubjectId };
2446
+ }
2447
+ return {
2448
+ agentOnly: true,
2449
+ viewerSubjectId: input.initiatingSubjectId,
2450
+ authorizedPersonalAttempt: input.agentAuthority ? {
2451
+ accountId: input.accountId,
2452
+ workspaceId: input.workspaceId,
2453
+ sessionId: input.agentAuthority.sessionId,
2454
+ attemptId: input.agentAuthority.attemptId
2455
+ } : void 0
2456
+ };
2457
+ }
1771
2458
  function documentAccessConditions(workspaceId, access) {
1772
2459
  const organization = eq(schema.documents.authorityKind, "organization");
1773
2460
  const workspace = and(
@@ -1775,20 +2462,45 @@ function documentAccessConditions(workspaceId, access) {
1775
2462
  eq(schema.documents.authorityWorkspaceId, workspaceId)
1776
2463
  );
1777
2464
  const viewer = cleanString(access?.viewerSubjectId ?? null);
2465
+ const personalAttempt = access?.authorizedPersonalAttempt;
2466
+ const authorizedPersonal = personalAttempt ? sql`${schema.documents.id} IN (
2467
+ SELECT resolve_session_attempt_personal_document_reads(
2468
+ ${personalAttempt.accountId}::uuid,
2469
+ ${personalAttempt.workspaceId}::uuid,
2470
+ ${personalAttempt.sessionId}::uuid,
2471
+ ${personalAttempt.attemptId}::uuid
2472
+ )
2473
+ )` : sql`false`;
1778
2474
  const personal = viewer ? and(
1779
2475
  eq(schema.documents.authorityKind, "personal"),
1780
- eq(schema.documents.authorityWorkspaceId, workspaceId),
1781
- eq(schema.documents.authoritySubjectId, viewer)
2476
+ eq(schema.documents.authoritySubjectId, viewer),
2477
+ access?.agentOnly ? or(
2478
+ and(
2479
+ isNull(schema.documents.authorityId),
2480
+ eq(schema.documents.authorityWorkspaceId, workspaceId)
2481
+ ),
2482
+ and(isNotNull(schema.documents.authorityId), authorizedPersonal)
2483
+ ) ?? authorizedPersonal : or(
2484
+ eq(schema.documents.authorityWorkspaceId, workspaceId),
2485
+ isNull(schema.documents.authorityWorkspaceId)
2486
+ ) ?? eq(schema.documents.authorityWorkspaceId, workspaceId)
1782
2487
  ) : void 0;
1783
2488
  const authority = viewer ? or(organization, workspace, personal) ?? organization : or(organization, workspace) ?? organization;
2489
+ const viewerSql = viewer ? sql`${viewer}` : sql`NULL::text`;
2490
+ const providerAuthorization = sql`google_drive_file_authorized(
2491
+ ${schema.documents.accountId},
2492
+ ${workspaceId}::uuid,
2493
+ ${viewerSql},
2494
+ ${schema.documents.fileId}
2495
+ )`;
1784
2496
  if (access?.agentOnly) {
1785
- return [eq(schema.documents.agentAccess, true), authority];
2497
+ return [eq(schema.documents.agentAccess, true), authority, providerAuthorization];
1786
2498
  }
1787
- return [authority];
2499
+ return [authority, providerAuthorization];
1788
2500
  }
1789
2501
  function documentMatchesAccess(document, workspaceId, access) {
1790
2502
  if (access?.agentOnly) {
1791
- return document.agentAccess && canViewDocument(document, access.viewerSubjectId, workspaceId);
2503
+ return document.agentAccess && (document.authorityKind !== "personal" || document.authorityId === null) && canViewDocument(document, access.viewerSubjectId, workspaceId);
1792
2504
  }
1793
2505
  return canViewDocument(document, access?.viewerSubjectId, workspaceId);
1794
2506
  }
@@ -1806,16 +2518,16 @@ function canViewDocument(document, viewerSubjectId, workspaceId) {
1806
2518
  return document.authorityWorkspaceId === null && document.authoritySubjectId === null;
1807
2519
  }
1808
2520
  const normalizedWorkspaceId = cleanString(workspaceId ?? null);
1809
- if (!normalizedWorkspaceId || document.authorityWorkspaceId !== normalizedWorkspaceId) {
2521
+ if (!normalizedWorkspaceId) {
1810
2522
  return false;
1811
2523
  }
1812
2524
  if (document.authorityKind === "workspace") {
1813
- return document.authoritySubjectId === null;
2525
+ return document.authorityWorkspaceId === normalizedWorkspaceId && document.authoritySubjectId === null;
1814
2526
  }
1815
2527
  if (document.authorityKind === "personal") {
1816
2528
  const authoritySubjectId = canonicalDocumentAuthoritySubject(document.authoritySubjectId);
1817
2529
  const viewer = canonicalDocumentAuthoritySubject(viewerSubjectId);
1818
- return !!authoritySubjectId && authoritySubjectId === viewer;
2530
+ return !!authoritySubjectId && authoritySubjectId === viewer && (document.authorityWorkspaceId === null || document.authorityWorkspaceId === normalizedWorkspaceId);
1819
2531
  }
1820
2532
  return false;
1821
2533
  }
@@ -1863,9 +2575,24 @@ function mapSearchRowBase(row) {
1863
2575
  aclTags: cleanStringArray(row.aclTags),
1864
2576
  authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
1865
2577
  authorityWorkspaceId: row.authorityWorkspaceId,
1866
- authoritySubjectId: row.authoritySubjectId
2578
+ authoritySubjectId: row.authoritySubjectId,
2579
+ citation: parseKnowledgeProviderCitation(row.citation)
1867
2580
  };
1868
2581
  }
2582
+ function googleDriveCitationProjection(workspaceId, access) {
2583
+ const viewer = cleanString(access?.viewerSubjectId ?? null);
2584
+ const viewerSql = viewer ? sql`${viewer}` : sql`NULL::text`;
2585
+ return sql`google_drive_document_citation(
2586
+ ${schema.documents.accountId},
2587
+ ${workspaceId}::uuid,
2588
+ ${viewerSql},
2589
+ ${schema.documents.id},
2590
+ ${schema.documents.fileId}
2591
+ )`;
2592
+ }
2593
+ function parseKnowledgeProviderCitation(value) {
2594
+ return value === null || value === void 0 ? null : KnowledgeProviderCitation.parse(value);
2595
+ }
1869
2596
  function mergeDocumentSearchRows(rows, mode) {
1870
2597
  const byChunk = /* @__PURE__ */ new Map();
1871
2598
  for (const row of rows) {
@@ -1890,7 +2617,7 @@ function mergeDocumentSearchRows(rows, mode) {
1890
2617
  matchType
1891
2618
  };
1892
2619
  }).sort(
1893
- (left, right) => right.score - left.score || (right.vectorScore ?? 0) - (left.vectorScore ?? 0) || (right.keywordScore ?? 0) - (left.keywordScore ?? 0) || left.chunkIndex - right.chunkIndex
2620
+ (left, right) => right.score - left.score || (right.vectorScore ?? 0) - (left.vectorScore ?? 0) || (right.keywordScore ?? 0) - (left.keywordScore ?? 0) || left.chunkIndex - right.chunkIndex || (left.chunkId === right.chunkId ? 0 : left.chunkId < right.chunkId ? -1 : 1)
1894
2621
  );
1895
2622
  }
1896
2623
  function combinedSearchScore(mode, vectorScore, keywordScore, matchType) {
@@ -1952,10 +2679,16 @@ function deterministicEmbedding(text, dimensions = DEFAULT_DOCUMENT_EMBEDDING_DI
1952
2679
  const norm = Math.hypot(...values) || 1;
1953
2680
  return values.map((value) => Number((value / norm).toFixed(6)));
1954
2681
  }
1955
- async function requireReadyFile(db, workspaceId, fileId) {
1956
- const file = await requireFile(db, workspaceId, fileId);
2682
+ async function requireReadyFile(db, input) {
2683
+ const [file] = await getFilesForSubject(db, {
2684
+ accountId: input.accountId,
2685
+ workspaceId: input.workspaceId,
2686
+ subjectId: input.subjectId,
2687
+ fileIds: [input.fileId]
2688
+ });
2689
+ if (!file) throw new Error(`File not found: ${input.fileId}`);
1957
2690
  if (file.status !== "ready") {
1958
- throw new Error(`File ${fileId} is ${file.status}`);
2691
+ throw new Error(`File ${input.fileId} is ${file.status}`);
1959
2692
  }
1960
2693
  return file;
1961
2694
  }
@@ -2071,6 +2804,7 @@ function mapDocument(row) {
2071
2804
  authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
2072
2805
  authorityWorkspaceId: row.authorityWorkspaceId,
2073
2806
  authoritySubjectId: row.authoritySubjectId,
2807
+ authorityId: row.authorityId,
2074
2808
  visibility: normalizeDocumentVisibility(row.visibility),
2075
2809
  createdBy: row.createdBy,
2076
2810
  agentAccess: row.agentAccess,
@@ -2175,10 +2909,14 @@ export {
2175
2909
  getDocument,
2176
2910
  getDocumentBase,
2177
2911
  getDocumentChunk,
2912
+ getDocumentForIndexing,
2178
2913
  getDocumentInventory,
2914
+ getDocumentOriginalFile,
2179
2915
  getEffectiveKnowledgeRecord,
2180
2916
  heuristicCuration,
2181
2917
  indexDocumentNow,
2918
+ listAccessibleDocuments,
2919
+ listDocumentAuthorityReclassifications,
2182
2920
  listDocumentBases,
2183
2921
  listDocumentBasesEnsuringDefault,
2184
2922
  listDocuments,
@@ -2188,9 +2926,15 @@ export {
2188
2926
  parseDocumentBytes,
2189
2927
  projectKnowledgeRecord,
2190
2928
  queueDocumentForReindex,
2929
+ reclassifyDocumentAuthority,
2191
2930
  resolveDocumentAuthority,
2931
+ resolveEffectiveDocumentAccess,
2932
+ runDocumentDefaultCollectionBackfill,
2192
2933
  searchDocuments,
2193
2934
  searchEffectiveDocuments,
2194
- searchEffectiveKnowledge
2935
+ searchEffectiveKnowledge,
2936
+ selectDocumentSearchCandidateWindow,
2937
+ selectKnowledgeBrowseRecords,
2938
+ selectKnowledgeSearchResults
2195
2939
  };
2196
2940
  //# sourceMappingURL=index.js.map