@startsimpli/integrations 0.2.2 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/integrations",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Provider-agnostic types, API clients, hooks, and UI primitives for email + calendar integrations across StartSimpli apps.",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -431,6 +431,28 @@ describe('extractCommitments', () => {
431
431
  expect(result.created).toBeGreaterThan(0);
432
432
  });
433
433
 
434
+ it('slices a sweep so one call fits the gateway, and says where to resume', async () => {
435
+ // A deployed tenant closes a request at 60s and one call over the whole
436
+ // corpus does not finish inside it (measured on tandem — 504). The sweep is
437
+ // therefore BATCHED INTO CALLS THAT FIT, never one call per document.
438
+ const corpus = createFixtureCorpusApi({ scope: TANDEM, documents: FIXTURE_TRACKED_DOCUMENTS });
439
+ const extractor = createFixtureExtractor();
440
+ const spy = vi.spyOn(extractor, 'extract');
441
+
442
+ const first = await extractCommitments({ scope: TANDEM, limit: 2 }, { corpus, extractor });
443
+ expect(first.documentsProcessed).toBe(2);
444
+ expect(first.totalDocuments).toBe(FIXTURE_TRACKED_DOCUMENTS.length);
445
+ expect(first.nextOffset).toBe(2);
446
+ expect(spy.mock.calls[0][0].documents).toHaveLength(2); // still N per call
447
+
448
+ const second = await extractCommitments(
449
+ { scope: TANDEM, offset: first.nextOffset!, limit: 2 },
450
+ { corpus, extractor },
451
+ );
452
+ expect(second.documentsProcessed).toBe(1);
453
+ expect(second.nextOffset).toBeNull();
454
+ });
455
+
434
456
  it('refuses outright when the session carries no workspace claim', async () => {
435
457
  const corpus = createFixtureCorpusApi({ leakScope: true, documents: FIXTURE_TRACKED_DOCUMENTS });
436
458
  const listDocuments = vi.spyOn(corpus, 'listDocuments');
@@ -196,6 +196,8 @@ describe('TrackerPage — what is outstanding', () => {
196
196
  describe('TrackerPage — running the extraction', () => {
197
197
  const RESULT = {
198
198
  documentsProcessed: 16,
199
+ totalDocuments: 16,
200
+ nextOffset: null,
199
201
  fromRules: 24,
200
202
  fromModel: 11,
201
203
  created: 30,
@@ -235,6 +235,17 @@ export interface ExtractRequest {
235
235
  scope: CorpusScope;
236
236
  /** Narrow the run to specific records. Omit to sweep the corpus. */
237
237
  documentIds?: string[];
238
+ /**
239
+ * Where this call starts in the in-scope corpus, and how many documents it
240
+ * may take. Omit both to do the whole thing in one model call.
241
+ *
242
+ * This exists for a deployment fact, not a design preference: the tandem ALB
243
+ * closes a request at 60 seconds, and one batched call over all sixteen real
244
+ * documents does not finish inside it (measured — 504). Slicing keeps every
245
+ * call batched over N documents while letting the caller walk the corpus.
246
+ */
247
+ offset?: number;
248
+ limit?: number;
238
249
  }
239
250
 
240
251
  export interface ExtractDeps {
@@ -258,12 +269,24 @@ export async function extractCommitments(
258
269
 
259
270
  const { documents: candidates, matched } = await readDocuments(corpus, documentLimit, maxPages);
260
271
  const { kept, dropped } = enforceScope(candidates, request.scope);
261
- const documents = request.documentIds?.length
272
+ const inScope = request.documentIds?.length
262
273
  ? kept.filter((document) => request.documentIds!.includes(document.id))
263
274
  : kept;
264
275
 
276
+ // The slice this call handles. `listDocuments` orders stably (the tenant's
277
+ // (org_id, entity_type, -created_at) index), so an offset means the same thing
278
+ // between calls of one sweep.
279
+ const offset = Math.max(0, request.offset ?? 0);
280
+ const documents = request.limit ? inScope.slice(offset, offset + request.limit) : inScope.slice(offset);
281
+ const consumed = offset + documents.length;
282
+ const nextOffset = consumed < inScope.length ? consumed : null;
283
+
265
284
  if (documents.length === 0) {
266
- return { ...empty(extractor, 'nothing in scope'), droppedOutOfScope: dropped.length };
285
+ return {
286
+ ...empty(extractor, 'nothing in scope'),
287
+ totalDocuments: inScope.length,
288
+ droppedOutOfScope: dropped.length,
289
+ };
267
290
  }
268
291
 
269
292
  // 3. rules first, so the model can be told what not to repeat.
@@ -317,6 +340,8 @@ export async function extractCommitments(
317
340
 
318
341
  return {
319
342
  documentsProcessed: documents.length,
343
+ totalDocuments: inScope.length,
344
+ nextOffset,
320
345
  fromRules: ruleDrafts.length,
321
346
  fromModel: modelDrafts.length,
322
347
  created: reconciled.created,
@@ -325,8 +350,8 @@ export async function extractCommitments(
325
350
  needsReview: resolutions.needsReview,
326
351
  droppedOutOfScope: dropped.length,
327
352
  provider: extractor.describe(),
328
- ...(matched > candidates.length ? { } : {}),
329
353
  };
354
+ void matched;
330
355
  }
331
356
 
332
357
  /**
@@ -424,6 +449,8 @@ function empty(extractor: CorpusExtractorLlm, _why: string): ExtractionResult {
424
449
  void _why;
425
450
  return {
426
451
  documentsProcessed: 0,
452
+ totalDocuments: 0,
453
+ nextOffset: null,
427
454
  fromRules: 0,
428
455
  fromModel: 0,
429
456
  created: 0,
@@ -565,12 +565,30 @@ export interface CommitmentTrackerApi extends CommitmentApi {
565
565
  * Run extraction over documents that have not been processed. Optional: a
566
566
  * read-only deployment omits it and the control does not render.
567
567
  */
568
- extract?(input: { scope: CorpusScope; documentIds?: string[] }): Promise<ExtractionResult>;
568
+ extract?(input: {
569
+ scope: CorpusScope;
570
+ documentIds?: string[];
571
+ /** Resume point for a batched sweep. See `ExtractionResult.nextOffset`. */
572
+ offset?: number;
573
+ limit?: number;
574
+ }): Promise<ExtractionResult>;
569
575
  }
570
576
 
571
577
  export interface ExtractionResult {
572
- /** Documents read and sent to the extractor. */
578
+ /** Documents read and sent to the extractor IN THIS CALL. */
573
579
  documentsProcessed: number;
580
+ /** Documents in scope altogether, so a partial sweep can say what it is of. */
581
+ totalDocuments: number;
582
+ /**
583
+ * Where a resumed sweep should start, or `null` when the corpus is done.
584
+ *
585
+ * A run is RESUMABLE because it has to be: a deployed tenant sits behind a
586
+ * gateway with a request timeout (60s on the tandem ALB, measured — a single
587
+ * call over sixteen documents 504s), and one model call per document is
588
+ * forbidden. So the sweep is batched into calls that fit, and the caller walks
589
+ * the offsets. Still N documents per model call; just not always all of them.
590
+ */
591
+ nextOffset: number | null;
574
592
  /** Items the vendor's own structure produced, with no model involved. */
575
593
  fromRules: number;
576
594
  /** Items only the model found. */