@startsimpli/integrations 0.1.3 → 0.1.5

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.
@@ -0,0 +1,486 @@
1
+ /**
2
+ * A multi-source fixture corpus, shipped as a TESTING UTILITY.
3
+ *
4
+ * WHY IT IS IN THE PACKAGE and not in a test file or a harness app: the vitest
5
+ * suite and the browser harness must exercise the same corpus, or "it renders"
6
+ * and "it passes" are claims about two different things. Same reasoning as
7
+ * `startsimpli_connectors.testing.load_fixture` on the backend.
8
+ *
9
+ * PROVENANCE. The Granola rows carry the note ids, titles, timestamps,
10
+ * attendees and transcript lines from
11
+ * `start-simpli-api/packages/startsimpli-connectors/tests/fixtures/granola/`,
12
+ * and the money rows carry the ids, amounts and currencies from the `stripe/`
13
+ * set next to it. Those upstream fixtures are themselves CONSTRUCTED, not
14
+ * captured — Granola publishes no sandbox — and their README says so; nothing
15
+ * here is more real than they are.
16
+ *
17
+ * THE DROPBOX AND DRIVE ROWS ARE NOT DECORATION. bd startsim-768w.38.6's
18
+ * standing test is "would this row, badge, filter or empty state still make
19
+ * sense if the only connected source were Dropbox?". A fixture set containing
20
+ * only meeting notes cannot answer that question, so this one contains a signed
21
+ * contract from Dropbox and a pricing sheet from Drive, with their long-form
22
+ * text in `sections` exactly the way a transcript arrives. Any screen that
23
+ * renders the Granola rows well and these badly has failed the test visibly.
24
+ *
25
+ * THE SECOND ORG IS THE SCOPE TEST. `northwind` holds a document that answers a
26
+ * `tandem` question BETTER than anything `tandem` owns. That is deliberate: it
27
+ * is the shape of the leak that matters — not a random foreign row, but the one
28
+ * a ranker would happily pick if nothing stopped it.
29
+ */
30
+ import type {
31
+ CorpusApi,
32
+ CorpusPage,
33
+ CorpusScope,
34
+ DocumentQuery,
35
+ MoneyEvent,
36
+ Party,
37
+ SourceDocument,
38
+ SourceFreshness,
39
+ } from './types';
40
+
41
+ /** The tenant every fixture document belongs to unless stated otherwise. */
42
+ export const FIXTURE_ORG = 'tandem';
43
+ /** A DIFFERENT customer workspace under the same foundry (bd startsim-768w.34.4). */
44
+ export const FIXTURE_OTHER_ORG = 'northwind';
45
+
46
+ const OWNER_A = 'usr_dana';
47
+ const OWNER_B = 'usr_sam';
48
+
49
+ const scope = (orgId: string, ownerSub: string): CorpusScope => ({ orgId, ownerSub });
50
+
51
+ const DANA = { id: 'party_dana', name: 'Dana Reed', kind: 'person', email: 'dana.reed@example.com' };
52
+ const SAM = { id: 'party_sam', name: 'Sam Ortiz', kind: 'person', email: 'sam.ortiz@example.com' };
53
+ const KIM = { id: 'party_kim', name: 'Kim Li', kind: 'person', email: 'kim.li@example.com' };
54
+ const ACME = { id: 'party_acme', name: 'Acme Robotics', kind: 'company', email: 'ada@example.com' };
55
+ const BETA = { id: 'party_beta', name: 'Beta Foods', kind: 'company', email: 'bo@example.com' };
56
+
57
+ export const FIXTURE_PARTIES: Party[] = [
58
+ { ...DANA, source: 'granola', sourceLabel: 'Granola', sourceRef: 'dana.reed@example.com', scope: scope(FIXTURE_ORG, OWNER_A) },
59
+ { ...SAM, source: 'granola', sourceLabel: 'Granola', sourceRef: 'sam.ortiz@example.com', scope: scope(FIXTURE_ORG, OWNER_A) },
60
+ { ...KIM, source: 'granola', sourceLabel: 'Granola', sourceRef: 'kim.li@example.com', scope: scope(FIXTURE_ORG, OWNER_A) },
61
+ { ...ACME, source: 'stripe', sourceLabel: 'Stripe', sourceRef: 'cus_RedSuite01Acme', scope: scope(FIXTURE_ORG, OWNER_A) },
62
+ { ...BETA, source: 'stripe', sourceLabel: 'Stripe', sourceRef: 'cus_RedSuite02Beta', scope: scope(FIXTURE_ORG, OWNER_A) },
63
+ ];
64
+
65
+ const GRANOLA_TRANSCRIPT = [
66
+ 'Dana Reed: Morning — let’s start with the connector.',
67
+ 'Sam Ortiz: Sounds good. I pulled the OpenAPI shapes yesterday.',
68
+ 'Dana Reed: The transcript is going on its own entity.',
69
+ 'Sam Ortiz: Agreed — fifty transcripts a page is not a page.',
70
+ 'Dana Reed: On renewal, Acme want the same rate for another twelve months.',
71
+ 'Sam Ortiz: Then we hold at four thousand a month and cap the seats at forty.',
72
+ ].join('\n');
73
+
74
+ export const FIXTURE_DOCUMENTS: SourceDocument[] = [
75
+ {
76
+ id: 'not_1d3tmYTlCICgjy',
77
+ title: 'Acme <> StartSimpli weekly sync',
78
+ body: 'Agreed to ship the connector RED suite first. Acme asked to renew at the same rate for another twelve months.',
79
+ sections: [{ key: 'transcript', label: 'Transcript', text: GRANOLA_TRANSCRIPT }],
80
+ occurredAt: '2026-01-27T15:30:00Z',
81
+ source: 'granola',
82
+ sourceLabel: 'Granola',
83
+ sourceRef: 'not_1d3tmYTlCICgjy',
84
+ url: 'https://notes.granola.ai/d/9f1c2b7e-2a44-4f3e-9a0d-6c1b8e5d4a21',
85
+ participants: [DANA, SAM, KIM, ACME],
86
+ scope: scope(FIXTURE_ORG, OWNER_A),
87
+ ingestedAt: '2026-01-27T16:10:00Z',
88
+ },
89
+ {
90
+ id: 'not_7Kq2ZbVnRt4aXm',
91
+ title: 'Design review',
92
+ body: 'Reviewed the two-entity split. Note and transcript become separate records so a page of fifty stays a page.',
93
+ sections: [],
94
+ occurredAt: '2026-01-26T11:00:00Z',
95
+ source: 'granola',
96
+ sourceLabel: 'Granola',
97
+ sourceRef: 'not_7Kq2ZbVnRt4aXm',
98
+ url: 'https://notes.granola.ai/d/2b7d41ce-8f05-4a19-b3d2-77e4c0a9f118',
99
+ participants: [DANA, SAM],
100
+ scope: scope(FIXTURE_ORG, OWNER_A),
101
+ ingestedAt: '2026-01-26T11:52:00Z',
102
+ },
103
+ {
104
+ // The upstream fixture has `title: null` and no calendar event. A row with
105
+ // no title is a real row and must still render — see `note_detail_no_calendar`.
106
+ id: 'not_Pd9wLs3EhY6cQz',
107
+ title: 'Untitled granola record',
108
+ body: 'Ad-hoc call, no calendar invite.',
109
+ sections: [],
110
+ occurredAt: '2026-01-25T09:15:00Z',
111
+ source: 'granola',
112
+ sourceLabel: 'Granola',
113
+ sourceRef: 'not_Pd9wLs3EhY6cQz',
114
+ participants: [DANA],
115
+ scope: scope(FIXTURE_ORG, OWNER_B),
116
+ ingestedAt: '2026-01-25T09:40:00Z',
117
+ },
118
+ {
119
+ // `note_detail_no_transcript.json`: the transcript was auto-deleted upstream
120
+ // and the summary is empty. A document with no readable text is the emptiest
121
+ // real row the Library will ever be handed.
122
+ id: 'not_Xa4mB8vTuN2kRe',
123
+ title: 'Board prep (transcript auto-deleted upstream)',
124
+ body: '',
125
+ sections: [],
126
+ occurredAt: '2026-01-20T13:00:00Z',
127
+ source: 'granola',
128
+ sourceLabel: 'Granola',
129
+ sourceRef: 'not_Xa4mB8vTuN2kRe',
130
+ participants: [DANA],
131
+ scope: scope(FIXTURE_ORG, OWNER_A),
132
+ ingestedAt: '2026-01-24T08:05:00Z',
133
+ },
134
+ {
135
+ // Dropbox: the second source John named, and the one the generic test is
136
+ // written against. Same type, same badge, same detail view, no new code.
137
+ id: 'dbx_9812f0',
138
+ title: 'Acme Robotics — Master Services Agreement (signed)',
139
+ body: 'Signed MSA between StartSimpli and Acme Robotics, effective 1 February 2026, initial term twelve months.',
140
+ sections: [
141
+ {
142
+ key: 'extracted_text',
143
+ label: 'Extracted text',
144
+ text: [
145
+ 'MASTER SERVICES AGREEMENT',
146
+ '1. Term. This Agreement begins on 1 February 2026 and continues for twelve (12) months.',
147
+ '2. Fees. Customer shall pay USD 4,000 per month, invoiced monthly in advance.',
148
+ '3. Seats. The subscription covers up to forty (40) named seats.',
149
+ '4. Renewal. This Agreement renews for successive twelve-month terms unless either party gives sixty (60) days notice.',
150
+ '5. Termination for convenience requires ninety (90) days written notice.',
151
+ ].join('\n'),
152
+ },
153
+ ],
154
+ occurredAt: '2026-02-01T00:00:00Z',
155
+ source: 'dropbox',
156
+ sourceLabel: 'Dropbox',
157
+ sourceRef: 'id:9812f0a4b1',
158
+ url: 'https://www.dropbox.example/scl/fi/9812f0a4b1/acme-msa-signed.pdf',
159
+ participants: [ACME, DANA],
160
+ scope: scope(FIXTURE_ORG, OWNER_A),
161
+ ingestedAt: '2026-02-02T06:00:00Z',
162
+ },
163
+ {
164
+ id: 'gdrive_1QpZ',
165
+ title: 'Rate card 2026',
166
+ body: 'Standard pricing for 2026. Platform tier USD 4,000 per month up to forty seats; overage USD 90 per seat per month.',
167
+ sections: [
168
+ {
169
+ key: 'extracted_text',
170
+ label: 'Extracted text',
171
+ text: 'Platform: USD 4,000/mo (40 seats). Overage: USD 90/seat/mo. Discounts require VP approval above 15%.',
172
+ },
173
+ ],
174
+ occurredAt: '2026-01-05T00:00:00Z',
175
+ source: 'gdrive',
176
+ sourceLabel: 'Google Drive',
177
+ sourceRef: '1QpZv7t9kR',
178
+ url: 'https://docs.google.example/document/d/1QpZv7t9kR',
179
+ participants: [DANA],
180
+ scope: scope(FIXTURE_ORG, OWNER_A),
181
+ ingestedAt: '2026-01-06T07:30:00Z',
182
+ },
183
+ {
184
+ // ---- A DIFFERENT CUSTOMER WORKSPACE ----
185
+ // Deliberately the BEST answer to "what did we agree with Acme on renewal":
186
+ // more specific, more recent, and full of exactly the terms a ranker loves.
187
+ // Nothing but the scope guard stands between it and a very convincing leak.
188
+ id: 'nw_contract_44',
189
+ title: 'Acme Robotics renewal — Northwind terms (CONFIDENTIAL)',
190
+ body: 'Northwind renewed Acme Robotics at USD 2,750 per month with unlimited seats and a ninety-day out. Do not share outside Northwind.',
191
+ sections: [
192
+ {
193
+ key: 'extracted_text',
194
+ label: 'Extracted text',
195
+ text: 'Renewal rate USD 2,750/mo. Unlimited seats. Ninety (90) day termination for convenience. Acme renewal signed 2026-02-10.',
196
+ },
197
+ ],
198
+ occurredAt: '2026-02-10T00:00:00Z',
199
+ source: 'dropbox',
200
+ sourceLabel: 'Dropbox',
201
+ sourceRef: 'id:nw44a2',
202
+ participants: [ACME],
203
+ scope: scope(FIXTURE_OTHER_ORG, 'usr_northwind'),
204
+ ingestedAt: '2026-02-10T09:00:00Z',
205
+ },
206
+ ];
207
+
208
+ export const FIXTURE_MONEY_EVENTS: MoneyEvent[] = [
209
+ {
210
+ id: 'in_RedSuiteUsd0001',
211
+ subtype: 'invoice',
212
+ amount: 500,
213
+ currency: 'usd',
214
+ status: 'paid',
215
+ occurredAt: '2026-08-01T00:00:00Z',
216
+ source: 'stripe',
217
+ sourceLabel: 'Stripe',
218
+ sourceRef: 'in_RedSuiteUsd0001',
219
+ url: 'https://invoice.stripe.example/i/in_RedSuiteUsd0001',
220
+ description: 'Subscription update proration',
221
+ party: BETA,
222
+ scope: scope(FIXTURE_ORG, OWNER_A),
223
+ },
224
+ {
225
+ id: 'ch_RedSuiteJpy0001',
226
+ subtype: 'charge',
227
+ amount: 1500,
228
+ currency: 'jpy',
229
+ status: 'refunded',
230
+ occurredAt: '2026-07-31T00:01:00Z',
231
+ source: 'stripe',
232
+ sourceLabel: 'Stripe',
233
+ sourceRef: 'ch_RedSuiteJpy0001',
234
+ description: 'Invoice REDSUITE-0001 — refunded in full',
235
+ party: ACME,
236
+ scope: scope(FIXTURE_ORG, OWNER_A),
237
+ },
238
+ {
239
+ id: 'sub_RedSuite01Acme',
240
+ subtype: 'subscription',
241
+ amount: 400000,
242
+ currency: 'usd',
243
+ status: 'active',
244
+ occurredAt: '2026-07-31T00:00:00Z',
245
+ source: 'stripe',
246
+ sourceLabel: 'Stripe',
247
+ sourceRef: 'sub_RedSuite01Acme',
248
+ description: 'Platform tier, monthly, forty seats',
249
+ party: ACME,
250
+ scope: scope(FIXTURE_ORG, OWNER_A),
251
+ },
252
+ ];
253
+
254
+ /**
255
+ * One row per connected source, including one that is broken and one that has
256
+ * never delivered — because a freshness strip that only ever says "ok" has never
257
+ * been looked at.
258
+ */
259
+ export const FIXTURE_SOURCES: SourceFreshness[] = [
260
+ {
261
+ source: 'granola',
262
+ label: 'Granola',
263
+ documentCount: 4,
264
+ lastSyncedAt: '2026-08-07T06:00:00Z',
265
+ status: 'ok',
266
+ },
267
+ {
268
+ source: 'dropbox',
269
+ label: 'Dropbox',
270
+ documentCount: 1,
271
+ lastSyncedAt: '2026-08-02T06:00:00Z',
272
+ status: 'stale',
273
+ detail: 'Last sync was more than 26 hours ago.',
274
+ },
275
+ {
276
+ source: 'gdrive',
277
+ label: 'Google Drive',
278
+ documentCount: 1,
279
+ lastSyncedAt: '2026-08-06T07:30:00Z',
280
+ status: 'error',
281
+ detail: 'The vendor rejected this key (401). Reconnect Google Drive.',
282
+ },
283
+ {
284
+ source: 'stripe',
285
+ label: 'Stripe',
286
+ documentCount: 0,
287
+ lastSyncedAt: null,
288
+ status: 'never',
289
+ detail: 'Connected, but nothing has arrived yet.',
290
+ },
291
+ ];
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // a CorpusApi over the fixtures
295
+ // ---------------------------------------------------------------------------
296
+
297
+ export interface FixtureCorpusOptions {
298
+ documents?: SourceDocument[];
299
+ moneyEvents?: MoneyEvent[];
300
+ parties?: Party[];
301
+ sources?: SourceFreshness[];
302
+ /** Page size when the caller does not ask for one. Small, to exercise paging. */
303
+ defaultPageSize?: number;
304
+ /**
305
+ * The scope this "server" enforces. Omit to model a CORRECT server that
306
+ * scopes every read; set `leakScope` to model a broken one and prove the
307
+ * client-side guard catches what the server should have.
308
+ */
309
+ scope?: CorpusScope;
310
+ /** Return rows from every org, as an over-broad server would. */
311
+ leakScope?: boolean;
312
+ /** Record every query the composers made, for assertions about server-side filtering. */
313
+ onQuery?: (endpoint: string, query: unknown) => void;
314
+ }
315
+
316
+ function haystack(document: SourceDocument): string {
317
+ return [
318
+ document.title,
319
+ document.body,
320
+ ...document.sections.map((section) => section.text),
321
+ ...document.participants.map((party) => `${party.name} ${party.email ?? ''}`),
322
+ document.sourceLabel,
323
+ ]
324
+ .join(' ')
325
+ .toLowerCase();
326
+ }
327
+
328
+ function paginate<T>(rows: T[], page: number, pageSize: number, applied: string[]): CorpusPage<T> {
329
+ const start = (page - 1) * pageSize;
330
+ return {
331
+ count: rows.length,
332
+ next: start + pageSize < rows.length ? String(page + 1) : null,
333
+ previous: page > 1 ? String(page - 1) : null,
334
+ results: rows.slice(start, start + pageSize),
335
+ appliedFilters: applied,
336
+ };
337
+ }
338
+
339
+ /**
340
+ * A `CorpusApi` that behaves the way the tenant API is SUPPOSED to: it filters
341
+ * and pages server-side and reports which filters it applied. It is the "server"
342
+ * in the harness and in the tests, which is what makes both of them a fair test
343
+ * of a composer that must never filter in the browser.
344
+ */
345
+ export function createFixtureCorpusApi(options: FixtureCorpusOptions = {}): CorpusApi {
346
+ const {
347
+ documents = FIXTURE_DOCUMENTS,
348
+ moneyEvents = FIXTURE_MONEY_EVENTS,
349
+ parties = FIXTURE_PARTIES,
350
+ sources = FIXTURE_SOURCES,
351
+ defaultPageSize = 5,
352
+ scope: serverScope,
353
+ leakScope = false,
354
+ onQuery,
355
+ } = options;
356
+
357
+ const inScope = <T extends { scope: CorpusScope }>(rows: T[]): T[] => {
358
+ if (leakScope || !serverScope) return rows;
359
+ return rows.filter(
360
+ (row) =>
361
+ row.scope.orgId === serverScope.orgId &&
362
+ (!serverScope.ownerSub || row.scope.ownerSub === serverScope.ownerSub),
363
+ );
364
+ };
365
+
366
+ return {
367
+ async listDocuments(query: DocumentQuery) {
368
+ onQuery?.('documents', query);
369
+ const applied: string[] = [];
370
+ let rows = inScope(documents);
371
+
372
+ if (query.source) {
373
+ applied.push('source');
374
+ rows = rows.filter((document) => document.source === query.source);
375
+ }
376
+ if (query.partyId) {
377
+ applied.push('partyId');
378
+ rows = rows.filter((document) =>
379
+ document.participants.some((party) => party.id === query.partyId),
380
+ );
381
+ }
382
+ if (query.occurredAfter) {
383
+ applied.push('occurredAfter');
384
+ rows = rows.filter((d) => (d.occurredAt ?? '') >= query.occurredAfter!);
385
+ }
386
+ if (query.occurredBefore) {
387
+ applied.push('occurredBefore');
388
+ rows = rows.filter((d) => (d.occurredAt ?? '') <= query.occurredBefore!);
389
+ }
390
+ if (query.search) {
391
+ applied.push('search');
392
+ const needles = query.search.toLowerCase().split(/\s+/).filter(Boolean);
393
+ rows = rows.filter((document) => {
394
+ const text = haystack(document);
395
+ return needles.some((needle) => text.includes(needle));
396
+ });
397
+ }
398
+
399
+ rows = [...rows].sort((a, b) => (b.occurredAt ?? '').localeCompare(a.occurredAt ?? ''));
400
+ return paginate(rows, query.page ?? 1, query.pageSize ?? defaultPageSize, applied);
401
+ },
402
+
403
+ async getDocument(id: string) {
404
+ const found = inScope(documents).find((document) => document.id === id);
405
+ if (!found) {
406
+ // A row outside the caller's scope is ABSENT, not forbidden — same
407
+ // convention as the tenant API, where a cross-scope id is a 404.
408
+ const error = new Error('No such document.') as Error & { status?: number };
409
+ error.status = 404;
410
+ throw error;
411
+ }
412
+ return found;
413
+ },
414
+
415
+ async listParties(query = {}) {
416
+ onQuery?.('parties', query);
417
+ let rows = inScope(parties);
418
+ const applied: string[] = [];
419
+ if (query.search) {
420
+ applied.push('search');
421
+ const needle = query.search.toLowerCase();
422
+ rows = rows.filter((party) => `${party.name} ${party.email ?? ''}`.toLowerCase().includes(needle));
423
+ }
424
+ return paginate(rows, query.page ?? 1, query.pageSize ?? 50, applied);
425
+ },
426
+
427
+ async listMoneyEvents(query = {}) {
428
+ onQuery?.('money', query);
429
+ let rows = inScope(moneyEvents);
430
+ const applied: string[] = [];
431
+ if (query.partyId) {
432
+ applied.push('partyId');
433
+ rows = rows.filter((event) => event.party?.id === query.partyId);
434
+ }
435
+ if (query.search) {
436
+ applied.push('search');
437
+ const needles = query.search.toLowerCase().split(/\s+/).filter(Boolean);
438
+ rows = rows.filter((event) => {
439
+ const text = `${event.subtype} ${event.status} ${event.description ?? ''} ${event.party?.name ?? ''}`.toLowerCase();
440
+ return needles.some((needle) => text.includes(needle));
441
+ });
442
+ }
443
+ return paginate(rows, query.page ?? 1, query.pageSize ?? 20, applied);
444
+ },
445
+
446
+ async listSources() {
447
+ return sources;
448
+ },
449
+ };
450
+ }
451
+
452
+ /**
453
+ * A `CorpusLlm` that answers WITHOUT a provider, by quoting the documents it was
454
+ * handed and citing every one it quoted.
455
+ *
456
+ * It exists so the retrieval, citation-resolution and scope machinery can be
457
+ * driven end to end — in vitest and in a browser — with no API key. It reports
458
+ * `live: false` from `describe()`, and every surface renders that, so a run
459
+ * powered by this can never be mistaken on screen for a run powered by a model.
460
+ */
461
+ export function createFixtureLlm(): import('./types').CorpusLlm {
462
+ return {
463
+ describe: () => ({ provider: 'fixture', model: 'fixture-echo', live: false }),
464
+ async answer(input) {
465
+ if (input.documents.length === 0) {
466
+ return { answer: '', citations: [], unanswerable: true };
467
+ }
468
+ const lines = input.documents.map((document) => {
469
+ const first = document.text.split('\n').find((line) => line.trim()) ?? document.title;
470
+ return `- ${document.title} (${document.source}): ${first.trim().slice(0, 180)}`;
471
+ });
472
+ const heading =
473
+ input.mode === 'generate'
474
+ ? `Draft assembled from ${input.documents.length} record(s):`
475
+ : `From ${input.documents.length} record(s) in your corpus:`;
476
+ return {
477
+ answer: [heading, ...lines].join('\n'),
478
+ citations: input.documents.map((document) => ({
479
+ documentId: document.id,
480
+ quote: document.text.slice(0, 120).trim(),
481
+ })),
482
+ unanswerable: false,
483
+ };
484
+ },
485
+ };
486
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The corpus layer (bd startsim-768w.38.6 Library, startsim-768w.38.7 Ask).
3
+ *
4
+ * Browser-safe: nothing reachable from here imports a provider client. The one
5
+ * module that does — `./llm` — is published separately as
6
+ * `@startsimpli/integrations/corpus-llm` and is server-only.
7
+ */
8
+ export type {
9
+ AnswerDraft,
10
+ AnswerRequest,
11
+ AskMode,
12
+ AskRequest,
13
+ AskResult,
14
+ AskStatus,
15
+ Citation,
16
+ CorpusApi,
17
+ CorpusAskApi,
18
+ CorpusLlm,
19
+ CorpusPage,
20
+ CorpusScope,
21
+ DocumentQuery,
22
+ DocumentSection,
23
+ LlmDescription,
24
+ MoneyEvent,
25
+ Party,
26
+ PartyRef,
27
+ PromptDocument,
28
+ RetrievedDocument,
29
+ SourceDocument,
30
+ SourceFreshness,
31
+ SourceKey,
32
+ } from './types';
33
+
34
+ export {
35
+ DEFAULT_TYPE_KEYS,
36
+ moneyEventAsDocument,
37
+ scopeOf,
38
+ toMoneyEvent,
39
+ toParty,
40
+ toPartyRef,
41
+ toSections,
42
+ toSourceDocument,
43
+ } from './entities';
44
+ export type { CorpusTypeKeys, TenantEntity } from './entities';
45
+
46
+ export {
47
+ enforceScope,
48
+ isInScope,
49
+ rankDocuments,
50
+ retrieveForQuestion,
51
+ snippetFor,
52
+ tokenize,
53
+ } from './retrieval';
54
+ export type { RetrieveOptions, ScopeFilterResult } from './retrieval';
55
+
56
+ export {
57
+ answerSystemPrompt,
58
+ askCorpus,
59
+ buildAnswerRequest,
60
+ documentText,
61
+ renderAnswerPrompt,
62
+ resolveCitations,
63
+ } from './ask';
64
+ export type { AskCorpusOptions } from './ask';
65
+
66
+ export { createCorpusApi } from './client';
67
+ export type { CorpusHttp, CreateCorpusApiOptions } from './client';
68
+
69
+ export {
70
+ createFixtureCorpusApi,
71
+ createFixtureLlm,
72
+ FIXTURE_DOCUMENTS,
73
+ FIXTURE_MONEY_EVENTS,
74
+ FIXTURE_ORG,
75
+ FIXTURE_OTHER_ORG,
76
+ FIXTURE_PARTIES,
77
+ FIXTURE_SOURCES,
78
+ } from './fixtures';
79
+ export type { FixtureCorpusOptions } from './fixtures';