@convex-dev/rag 0.3.0 → 0.3.2

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.
Files changed (42) hide show
  1. package/README.md +463 -121
  2. package/dist/client/defaultChunker.d.ts.map +1 -1
  3. package/dist/client/defaultChunker.js +47 -16
  4. package/dist/client/defaultChunker.js.map +1 -1
  5. package/dist/client/fileUtils.d.ts +4 -2
  6. package/dist/client/fileUtils.d.ts.map +1 -1
  7. package/dist/client/fileUtils.js +5 -3
  8. package/dist/client/fileUtils.js.map +1 -1
  9. package/dist/client/index.d.ts +19 -15
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +15 -11
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +11 -4
  14. package/dist/component/chunks.d.ts +1 -0
  15. package/dist/component/chunks.d.ts.map +1 -1
  16. package/dist/component/chunks.js +2 -1
  17. package/dist/component/chunks.js.map +1 -1
  18. package/dist/component/entries.d.ts +8 -8
  19. package/dist/component/entries.d.ts.map +1 -1
  20. package/dist/component/entries.js +30 -17
  21. package/dist/component/entries.js.map +1 -1
  22. package/dist/component/namespaces.d.ts +3 -3
  23. package/dist/component/namespaces.js +4 -4
  24. package/dist/component/namespaces.js.map +1 -1
  25. package/dist/component/schema.d.ts +31 -31
  26. package/dist/shared.d.ts +28 -11
  27. package/dist/shared.d.ts.map +1 -1
  28. package/dist/shared.js +2 -1
  29. package/dist/shared.js.map +1 -1
  30. package/package.json +1 -6
  31. package/src/client/defaultChunker.test.ts +1 -1
  32. package/src/client/defaultChunker.ts +73 -17
  33. package/src/client/fileUtils.ts +8 -4
  34. package/src/client/index.test.ts +28 -24
  35. package/src/client/index.ts +30 -24
  36. package/src/component/_generated/api.d.ts +11 -4
  37. package/src/component/chunks.test.ts +2 -0
  38. package/src/component/chunks.ts +2 -1
  39. package/src/component/entries.test.ts +16 -16
  40. package/src/component/entries.ts +31 -19
  41. package/src/component/namespaces.ts +4 -4
  42. package/src/shared.ts +15 -7
@@ -133,13 +133,17 @@ export function guessMimeTypeFromContents(buf: ArrayBuffer | string): string {
133
133
  /**
134
134
  * Make a contentHash of a Blob that matches the File Storage metadata, allowing
135
135
  * identifying when content is identical.
136
+ * By default, uses SHA-256 (which is what Convex File Storage tracks).
137
+ * Git / GitHub use SHA-1.
136
138
  * @param blob The contents to hash
137
- * @returns sha256 hash of the contents
139
+ * @returns hash of the contents
138
140
  */
139
-
140
- export async function contentHashFromArrayBuffer(buffer: ArrayBuffer) {
141
+ export async function contentHashFromArrayBuffer(
142
+ buffer: ArrayBuffer,
143
+ algorithm: "SHA-256" | "SHA-1" = "SHA-256"
144
+ ) {
141
145
  return Array.from(
142
- new Uint8Array(await crypto.subtle.digest("SHA-256", buffer))
146
+ new Uint8Array(await crypto.subtle.digest(algorithm, buffer))
143
147
  )
144
148
  .map((b) => b.toString(16).padStart(2, "0"))
145
149
  .join("");
@@ -32,10 +32,10 @@ const rag = new RAG(components.rag, {
32
32
  filterNames: ["simpleString", "arrayOfStrings", "customObject"],
33
33
  });
34
34
 
35
- export const findExistingEntryByContentHash = query({
35
+ export const findEntryByContentHash = query({
36
36
  args: { namespace: v.string(), key: v.string(), contentHash: v.string() },
37
37
  handler: async (ctx, args) => {
38
- return rag.findExistingEntryByContentHash(ctx, {
38
+ return rag.findEntryByContentHash(ctx, {
39
39
  namespace: args.namespace,
40
40
  key: args.key,
41
41
  contentHash: args.contentHash,
@@ -95,7 +95,7 @@ export const search = action({
95
95
  },
96
96
  handler: async (ctx, args) => {
97
97
  const { results, entries, text } = await rag.search(ctx, {
98
- embedding: args.embedding,
98
+ query: args.embedding,
99
99
  namespace: args.namespace,
100
100
  limit: args.limit ?? 10,
101
101
  chunkContext: args.chunkContext ?? { before: 0, after: 0 },
@@ -111,7 +111,7 @@ export const search = action({
111
111
 
112
112
  const testApi: ApiFromModules<{
113
113
  fns: {
114
- findExistingEntryByContentHash: typeof findExistingEntryByContentHash;
114
+ findEntryByContentHash: typeof findEntryByContentHash;
115
115
  add: typeof add;
116
116
  search: typeof search;
117
117
  };
@@ -200,16 +200,16 @@ describe("RAG thick client", () => {
200
200
  limit: 10,
201
201
  });
202
202
 
203
- // Should match README format: "# Title:\n{entry.text}"
204
- expect(text).toContain("# Test Document:");
203
+ // Should match README format: "## Title:\n{entry.text}"
204
+ expect(text).toContain("## Test Document:");
205
205
  expect(entries).toHaveLength(1);
206
206
  expect(entries[0].text).toBe(
207
207
  "Chunk 1 content\nChunk 2 content\nChunk 3 content"
208
208
  );
209
209
 
210
- // Overall text should be: "# Test Document:\nChunk 1 content\nChunk 2 content\nChunk 3 content"
210
+ // Overall text should be: "## Test Document:\nChunk 1 content\nChunk 2 content\nChunk 3 content"
211
211
  expect(text).toBe(
212
- "# Test Document:\nChunk 1 content\nChunk 2 content\nChunk 3 content"
212
+ "## Test Document:\n\nChunk 1 content\nChunk 2 content\nChunk 3 content"
213
213
  );
214
214
  });
215
215
 
@@ -235,8 +235,8 @@ describe("RAG thick client", () => {
235
235
  limit: 10,
236
236
  });
237
237
 
238
- // Should not have "# " prefix since no title
239
- expect(text).not.toContain("# ");
238
+ // Should not have "## " prefix since no title
239
+ expect(text).not.toContain("## ");
240
240
  expect(entries).toHaveLength(1);
241
241
  expect(entries[0].text).toBe("Content without title");
242
242
  expect(text).toBe("Content without title");
@@ -337,11 +337,11 @@ describe("RAG thick client", () => {
337
337
 
338
338
  // Should have entries separated by "\n---\n" as per README
339
339
  expect(text).toContain("---");
340
- expect(text).toMatch(/# .+:\n.+\n---\n# .+:\n.+/);
340
+ expect(text).toMatch(/## .+:\n\n.+\n\n---\n\n## .+:\n\n.+/);
341
341
 
342
- // Should have both titles prefixed with "# "
343
- expect(text).toContain("# First Document:");
344
- expect(text).toContain("# Second Document:");
342
+ // Should have both titles prefixed with "## "
343
+ expect(text).toContain("## First Document:");
344
+ expect(text).toContain("## Second Document:");
345
345
 
346
346
  expect(entries).toHaveLength(2);
347
347
  });
@@ -384,14 +384,14 @@ describe("RAG thick client", () => {
384
384
 
385
385
  // Should properly handle mixed formatting
386
386
  expect(text).toContain("---"); // Entries should be separated
387
- expect(text).toContain("# Titled Document:"); // Titled entry should have prefix
387
+ expect(text).toContain("## Titled Document:"); // Titled entry should have prefix
388
388
 
389
389
  // One entry should have title format, one should not
390
390
  const parts = text.split("\n---\n");
391
391
  expect(parts).toHaveLength(2);
392
392
 
393
- const hasTitle = parts.some((part) => part.startsWith("# "));
394
- const hasNoTitle = parts.some((part) => !part.startsWith("# "));
393
+ const hasTitle = parts.some((part) => part.startsWith("## "));
394
+ const hasNoTitle = parts.some((part) => !part.startsWith("## "));
395
395
  expect(hasTitle).toBe(true);
396
396
  expect(hasNoTitle).toBe(true);
397
397
 
@@ -445,28 +445,32 @@ describe("RAG thick client", () => {
445
445
  });
446
446
 
447
447
  // Verify basic structure matches README
448
- expect(text).toContain("# Title 1:");
449
- expect(text).toContain("# Title 2:");
448
+ expect(text).toContain("## Title 1:");
449
+ expect(text).toContain("## Title 2:");
450
450
  expect(text).toContain("---");
451
451
 
452
452
  // Should have proper entry separation
453
- const parts = text.split("\n---\n");
453
+ const parts = text.split("\n\n---\n\n");
454
454
  expect(parts).toHaveLength(2);
455
455
 
456
- // Each part should start with "# Title X:"
456
+ // Each part should start with "## Title X:"
457
457
  parts.forEach((part) => {
458
- expect(part).toMatch(/^# Title \d+:/);
458
+ expect(part).toMatch(/^## Title \d+:/);
459
459
  });
460
460
 
461
461
  expect(entries).toHaveLength(2);
462
462
 
463
463
  // Individual entry texts should follow the pattern
464
464
  expect(text).toBe(
465
- `# Title 1:
465
+ `## Title 1:
466
+
466
467
  Chunk 1 contents
467
468
  Chunk 2 contents
469
+
468
470
  ---
469
- # Title 2:
471
+
472
+ ## Title 2:
473
+
470
474
  Chunk 3 contents
471
475
  Chunk 4 contents`
472
476
  );
@@ -27,7 +27,7 @@ import {
27
27
  type Chunk,
28
28
  type CreateChunkArgs,
29
29
  type Entry,
30
- type EntryFilterValues,
30
+ type EntryFilter,
31
31
  type EntryId,
32
32
  type Namespace,
33
33
  type NamespaceId,
@@ -66,6 +66,7 @@ export type {
66
66
  export {
67
67
  type VEntry,
68
68
  type VSearchEntry,
69
+ type EntryFilter,
69
70
  vEntry,
70
71
  vSearchEntry,
71
72
  vSearchResult,
@@ -155,7 +156,7 @@ export class RAG<
155
156
  entryId: EntryId;
156
157
  status: Status;
157
158
  created: boolean;
158
- replacedVersion: Entry<FitlerSchemas, EntryMetadata> | null;
159
+ replacedEntry: Entry<FitlerSchemas, EntryMetadata> | null;
159
160
  }> {
160
161
  let namespaceId: NamespaceId;
161
162
  if ("namespaceId" in args) {
@@ -182,7 +183,7 @@ export class RAG<
182
183
  const onComplete =
183
184
  args.onComplete && (await createFunctionHandle(args.onComplete));
184
185
 
185
- const { entryId, status, created, replacedVersion } = await ctx.runMutation(
186
+ const { entryId, status, created, replacedEntry } = await ctx.runMutation(
186
187
  this.component.entries.add,
187
188
  {
188
189
  entry: {
@@ -203,7 +204,7 @@ export class RAG<
203
204
  entryId: entryId as EntryId,
204
205
  status,
205
206
  created,
206
- replacedVersion: replacedVersion as Entry<
207
+ replacedEntry: replacedEntry as Entry<
207
208
  FitlerSchemas,
208
209
  EntryMetadata
209
210
  > | null,
@@ -243,7 +244,7 @@ export class RAG<
243
244
  entryId: entryId as EntryId,
244
245
  status: "replaced" as const,
245
246
  created: false,
246
- replacedVersion: null,
247
+ replacedEntry: null,
247
248
  };
248
249
  }
249
250
  startOrder = nextStartOrder;
@@ -256,7 +257,7 @@ export class RAG<
256
257
  return {
257
258
  entryId: entryId as EntryId,
258
259
  status: "ready" as const,
259
- replacedVersion: promoted.replacedVersion as Entry<
260
+ replacedEntry: promoted.replacedEntry as Entry<
260
261
  FitlerSchemas,
261
262
  EntryMetadata
262
263
  > | null,
@@ -362,7 +363,7 @@ export class RAG<
362
363
  /**
363
364
  * The query to search for. Optional if embedding is provided.
364
365
  */
365
- query?: string;
366
+ query: string | Array<number>;
366
367
  } & SearchOptions<FitlerSchemas>
367
368
  ): Promise<{
368
369
  results: SearchResult[];
@@ -376,7 +377,7 @@ export class RAG<
376
377
  chunkContext = { before: 0, after: 0 },
377
378
  vectorScoreThreshold,
378
379
  } = args;
379
- let embedding = args.embedding;
380
+ let embedding = Array.isArray(args.query) ? args.query : undefined;
380
381
  if (!embedding) {
381
382
  const embedResult = await embed({
382
383
  model: this.options.textEmbeddingModel,
@@ -405,7 +406,7 @@ export class RAG<
405
406
  for (const range of ranges) {
406
407
  if (previousEnd !== 0) {
407
408
  if (range.startOrder !== previousEnd) {
408
- text += "\n...\n";
409
+ text += "\n\n...\n\n";
409
410
  } else {
410
411
  text += "\n";
411
412
  }
@@ -419,8 +420,8 @@ export class RAG<
419
420
  return {
420
421
  results: results as SearchResult[],
421
422
  text: entriesWithTexts
422
- .map((e) => (e.title ? `# ${e.title}:\n${e.text}` : e.text))
423
- .join(`\n---\n`),
423
+ .map((e) => (e.title ? `## ${e.title}:\n\n${e.text}` : e.text))
424
+ .join(`\n\n---\n\n`),
424
425
  entries: entriesWithTexts,
425
426
  };
426
427
  }
@@ -446,6 +447,11 @@ export class RAG<
446
447
  * The namespace to search in. e.g. a userId if entries are per-user.
447
448
  */
448
449
  namespace: string;
450
+ /**
451
+ * The text or embedding to search for. If provided, it will be used
452
+ * instead of the prompt for vector search.
453
+ */
454
+ query?: string | Array<number>;
449
455
  };
450
456
  /**
451
457
  * Required. The prompt to use for context search, as well as the final
@@ -551,15 +557,18 @@ export class RAG<
551
557
  async list(
552
558
  ctx: RunQueryCtx,
553
559
  args: {
554
- namespaceId: NamespaceId;
555
- paginationOpts: PaginationOptions;
560
+ namespaceId?: NamespaceId;
556
561
  order?: "desc" | "asc";
557
562
  status?: Status;
558
- }
563
+ } & ({ paginationOpts: PaginationOptions } | { limit: number })
559
564
  ): Promise<PaginationResult<Entry<FitlerSchemas, EntryMetadata>>> {
565
+ const paginationOpts =
566
+ "paginationOpts" in args
567
+ ? args.paginationOpts
568
+ : { cursor: null, numItems: args.limit };
560
569
  const results = await ctx.runQuery(this.component.entries.list, {
561
570
  namespaceId: args.namespaceId,
562
- paginationOpts: args.paginationOpts,
571
+ paginationOpts,
563
572
  order: args.order ?? "asc",
564
573
  status: args.status ?? "ready",
565
574
  });
@@ -586,7 +595,7 @@ export class RAG<
586
595
  * new results into a new entry when migrating, or avoiding duplicating work
587
596
  * when updating content.
588
597
  */
589
- async findExistingEntryByContentHash(
598
+ async findEntryByContentHash(
590
599
  ctx: RunQueryCtx,
591
600
  args: {
592
601
  namespace: string;
@@ -679,11 +688,13 @@ export class RAG<
679
688
  args: {
680
689
  paginationOpts: PaginationOptions;
681
690
  entryId: EntryId;
691
+ order?: "desc" | "asc";
682
692
  }
683
693
  ): Promise<PaginationResult<Chunk>> {
684
694
  return ctx.runQuery(this.component.chunks.list, {
685
695
  entryId: args.entryId,
686
696
  paginationOpts: args.paginationOpts,
697
+ order: args.order ?? "asc",
687
698
  });
688
699
  }
689
700
 
@@ -927,7 +938,7 @@ async function createChunkArgsBatch(
927
938
  for (const batch of makeBatches(missingEmbeddingsWithIndex, 100)) {
928
939
  const { embeddings } = await embedMany({
929
940
  model: embedModel,
930
- values: batch.map((b) => b.text),
941
+ values: batch.map((b) => b.text.trim() || "<empty>"),
931
942
  });
932
943
  for (const [index, embedding] of embeddings.entries()) {
933
944
  argsMaybeMissingEmbeddings[batch[index].index].embedding = embedding;
@@ -1060,7 +1071,7 @@ type EntryArgs<
1060
1071
  * and searching with the same value will return entries that match that
1061
1072
  * value exactly.
1062
1073
  */
1063
- filterValues?: EntryFilterValues<FitlerSchemas>[];
1074
+ filterValues?: EntryFilter<FitlerSchemas>[];
1064
1075
  /**
1065
1076
  * The importance of the entry. This is used to scale the vector search
1066
1077
  * score of each chunk.
@@ -1080,11 +1091,6 @@ type EntryArgs<
1080
1091
  };
1081
1092
 
1082
1093
  type SearchOptions<FitlerSchemas extends Record<string, Value>> = {
1083
- /**
1084
- * The embedding to search for. If provided, it will be used instead
1085
- * of the query for vector search.
1086
- */
1087
- embedding?: Array<number>;
1088
1094
  /**
1089
1095
  * Filters to apply to the search. These are OR'd together. To represent
1090
1096
  * AND logic, your filter can be an object or array with multiple values.
@@ -1097,7 +1103,7 @@ type SearchOptions<FitlerSchemas extends Record<string, Value>> = {
1097
1103
  * `{ team_user: { team: "team1", user: "user1" } }`, it will not match
1098
1104
  * `{ team_user: { team: "team1" } }` but it will match
1099
1105
  */
1100
- filters?: EntryFilterValues<FitlerSchemas>[];
1106
+ filters?: EntryFilter<FitlerSchemas>[];
1101
1107
  /**
1102
1108
  * The maximum number of messages to fetch. Default is 10.
1103
1109
  * This is the number *before* the chunkContext is applied.
@@ -62,6 +62,7 @@ export type Mounts = {
62
62
  "public",
63
63
  {
64
64
  entryId: string;
65
+ order: "desc" | "asc";
65
66
  paginationOpts: {
66
67
  cursor: string | null;
67
68
  endCursor?: string | null;
@@ -115,13 +116,14 @@ export type Mounts = {
115
116
  {
116
117
  created: boolean;
117
118
  entryId: string;
118
- replacedVersion: {
119
+ replacedEntry: {
119
120
  contentHash?: string;
120
121
  entryId: string;
121
122
  filterValues: Array<{ name: string; value: any }>;
122
123
  importance: number;
123
124
  key?: string;
124
125
  metadata?: Record<string, any>;
126
+ replacedAt?: number;
125
127
  status: "pending" | "ready" | "replaced";
126
128
  title?: string;
127
129
  } | null;
@@ -170,6 +172,7 @@ export type Mounts = {
170
172
  importance: number;
171
173
  key?: string;
172
174
  metadata?: Record<string, any>;
175
+ replacedAt?: number;
173
176
  status: "pending" | "ready" | "replaced";
174
177
  title?: string;
175
178
  } | null
@@ -185,6 +188,7 @@ export type Mounts = {
185
188
  importance: number;
186
189
  key?: string;
187
190
  metadata?: Record<string, any>;
191
+ replacedAt?: number;
188
192
  status: "pending" | "ready" | "replaced";
189
193
  title?: string;
190
194
  } | null
@@ -193,7 +197,7 @@ export type Mounts = {
193
197
  "query",
194
198
  "public",
195
199
  {
196
- namespaceId: string;
200
+ namespaceId?: string;
197
201
  order?: "desc" | "asc";
198
202
  paginationOpts: {
199
203
  cursor: string | null;
@@ -215,6 +219,7 @@ export type Mounts = {
215
219
  importance: number;
216
220
  key?: string;
217
221
  metadata?: Record<string, any>;
222
+ replacedAt?: number;
218
223
  status: "pending" | "ready" | "replaced";
219
224
  title?: string;
220
225
  }>;
@@ -227,13 +232,14 @@ export type Mounts = {
227
232
  "public",
228
233
  { entryId: string },
229
234
  {
230
- replacedVersion: {
235
+ replacedEntry: {
231
236
  contentHash?: string;
232
237
  entryId: string;
233
238
  filterValues: Array<{ name: string; value: any }>;
234
239
  importance: number;
235
240
  key?: string;
236
241
  metadata?: Record<string, any>;
242
+ replacedAt?: number;
237
243
  status: "pending" | "ready" | "replaced";
238
244
  title?: string;
239
245
  } | null;
@@ -321,7 +327,7 @@ export type Mounts = {
321
327
  "public",
322
328
  { namespaceId: string },
323
329
  {
324
- replacedVersion: null | {
330
+ replacedNamespace: null | {
325
331
  createdAt: number;
326
332
  dimension: number;
327
333
  filterNames: Array<string>;
@@ -355,6 +361,7 @@ export type Mounts = {
355
361
  importance: number;
356
362
  key?: string;
357
363
  metadata?: Record<string, any>;
364
+ replacedAt?: number;
358
365
  status: "pending" | "ready" | "replaced";
359
366
  title?: string;
360
367
  }>;
@@ -397,6 +397,7 @@ describe("chunks", () => {
397
397
  // Test listing with pagination
398
398
  const result = await t.query(api.chunks.list, {
399
399
  entryId,
400
+ order: "asc",
400
401
  paginationOpts: { numItems: 3, cursor: null },
401
402
  });
402
403
 
@@ -417,6 +418,7 @@ describe("chunks", () => {
417
418
  // Get next page
418
419
  const nextResult = await t.query(api.chunks.list, {
419
420
  entryId,
421
+ order: "asc",
420
422
  paginationOpts: { numItems: 3, cursor: result.continueCursor },
421
423
  });
422
424
 
@@ -463,6 +463,7 @@ export const list = query({
463
463
  args: v.object({
464
464
  entryId: v.id("entries"),
465
465
  paginationOpts: paginationOptsValidator,
466
+ order: v.union(v.literal("desc"), v.literal("asc")),
466
467
  }),
467
468
  returns: vPaginationResult(vChunk),
468
469
  handler: async (ctx, args) => {
@@ -470,7 +471,7 @@ export const list = query({
470
471
  const chunks = await paginator(ctx.db, schema)
471
472
  .query("chunks")
472
473
  .withIndex("entryId_order", (q) => q.eq("entryId", entryId))
473
- .order("asc")
474
+ .order(args.order)
474
475
  .paginate(paginationOpts);
475
476
  return {
476
477
  ...chunks,
@@ -46,7 +46,7 @@ describe("entries", () => {
46
46
  expect(result.created).toBe(true);
47
47
  expect(result.status).toBe("ready");
48
48
  expect(result.entryId).toBeDefined();
49
- expect(result.replacedVersion).toBeNull();
49
+ expect(result.replacedEntry).toBeNull();
50
50
 
51
51
  // Verify the entry was actually created
52
52
  const createdDoc = await t.run(async (ctx) => {
@@ -73,7 +73,7 @@ describe("entries", () => {
73
73
 
74
74
  expect(firstResult.created).toBe(true);
75
75
  expect(firstResult.status).toBe("ready");
76
- expect(firstResult.replacedVersion).toBeNull();
76
+ expect(firstResult.replacedEntry).toBeNull();
77
77
 
78
78
  // Second add with identical content
79
79
  const secondResult = await t.mutation(api.entries.add, {
@@ -84,7 +84,7 @@ describe("entries", () => {
84
84
  expect(secondResult.created).toBe(false);
85
85
  expect(secondResult.status).toBe("ready");
86
86
  expect(secondResult.entryId).toBe(firstResult.entryId);
87
- expect(secondResult.replacedVersion).toBeNull();
87
+ expect(secondResult.replacedEntry).toBeNull();
88
88
 
89
89
  // Verify no new entry was created
90
90
  const allDocs = await t.run(async (ctx) => {
@@ -116,7 +116,7 @@ describe("entries", () => {
116
116
  });
117
117
 
118
118
  expect(firstResult.created).toBe(true);
119
- expect(firstResult.replacedVersion).toBeNull();
119
+ expect(firstResult.replacedEntry).toBeNull();
120
120
 
121
121
  // Second add with different content hash
122
122
  const modifiedEntry = {
@@ -131,9 +131,9 @@ describe("entries", () => {
131
131
 
132
132
  expect(secondResult.created).toBe(true);
133
133
  expect(secondResult.entryId).not.toBe(firstResult.entryId);
134
- // When creating a entry as "ready" initially, replacedVersion is null
134
+ // When creating a entry as "ready" initially, replacedEntry is null
135
135
  // Replacement only happens during pending -> ready transitions
136
- expect(secondResult.replacedVersion).toMatchObject({
136
+ expect(secondResult.replacedEntry).toMatchObject({
137
137
  entryId: firstResult.entryId,
138
138
  });
139
139
 
@@ -181,7 +181,7 @@ describe("entries", () => {
181
181
 
182
182
  expect(secondResult.created).toBe(true);
183
183
  expect(secondResult.entryId).not.toBe(firstResult.entryId);
184
- expect(secondResult.replacedVersion).toMatchObject({
184
+ expect(secondResult.replacedEntry).toMatchObject({
185
185
  entryId: firstResult.entryId,
186
186
  });
187
187
 
@@ -219,7 +219,7 @@ describe("entries", () => {
219
219
 
220
220
  expect(secondResult.created).toBe(true);
221
221
  expect(secondResult.entryId).not.toBe(firstResult.entryId);
222
- expect(secondResult.replacedVersion).toMatchObject({
222
+ expect(secondResult.replacedEntry).toMatchObject({
223
223
  entryId: firstResult.entryId,
224
224
  });
225
225
 
@@ -247,7 +247,7 @@ describe("entries", () => {
247
247
 
248
248
  expect(result.created).toBe(true);
249
249
  expect(result.status).toBe("pending");
250
- expect(result.replacedVersion).toBeNull();
250
+ expect(result.replacedEntry).toBeNull();
251
251
 
252
252
  // Verify the entry was created with pending status
253
253
  const createdDoc = await t.run(async (ctx) => {
@@ -277,8 +277,8 @@ describe("entries", () => {
277
277
  expect(result1.created).toBe(true);
278
278
  expect(result2.created).toBe(true);
279
279
  expect(result1.entryId).not.toBe(result2.entryId);
280
- expect(result1.replacedVersion).toBeNull();
281
- expect(result2.replacedVersion).toBeNull();
280
+ expect(result1.replacedEntry).toBeNull();
281
+ expect(result2.replacedEntry).toBeNull();
282
282
 
283
283
  // Verify both entries exist
284
284
  const allDocs = await t.run(async (ctx) => {
@@ -293,7 +293,7 @@ describe("entries", () => {
293
293
  expect(keys).toEqual(["doc1", "doc2"]);
294
294
  });
295
295
 
296
- test("pending to ready transition populates replacedVersion", async () => {
296
+ test("pending to ready transition populates replacedEntry", async () => {
297
297
  const t = convexTest(schema, modules);
298
298
  const namespaceId = await setupTestNamespace(t);
299
299
 
@@ -307,7 +307,7 @@ describe("entries", () => {
307
307
 
308
308
  expect(firstResult.created).toBe(true);
309
309
  expect(firstResult.status).toBe("ready");
310
- expect(firstResult.replacedVersion).toBeNull();
310
+ expect(firstResult.replacedEntry).toBeNull();
311
311
 
312
312
  // Second add - create as pending (no allChunks)
313
313
  const modifiedEntry = {
@@ -322,15 +322,15 @@ describe("entries", () => {
322
322
 
323
323
  expect(pendingResult.created).toBe(true);
324
324
  expect(pendingResult.status).toBe("pending");
325
- expect(pendingResult.replacedVersion).toBeNull();
325
+ expect(pendingResult.replacedEntry).toBeNull();
326
326
 
327
327
  // Promote to ready - this should replace the first entry
328
328
  const promoteResult = await t.mutation(api.entries.promoteToReady, {
329
329
  entryId: pendingResult.entryId,
330
330
  });
331
331
 
332
- expect(promoteResult.replacedVersion).not.toBeNull();
333
- expect(promoteResult.replacedVersion!.entryId).toBe(firstResult.entryId);
332
+ expect(promoteResult.replacedEntry).not.toBeNull();
333
+ expect(promoteResult.replacedEntry!.entryId).toBe(firstResult.entryId);
334
334
 
335
335
  // Verify the first entry is now replaced
336
336
  const firstDoc = await t.run(async (ctx) => {