@startsimpli/integrations 0.1.4 → 0.1.6

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,354 @@
1
+ /**
2
+ * The corpus contract — the typed layer between the tenant data API and the two
3
+ * product surfaces (bd startsim-768w.38.6 Library, startsim-768w.38.7 Ask).
4
+ *
5
+ * WHY A CONTRACT AND NOT A CLIENT: the composers take an INJECTED api the same
6
+ * way `ConnectionsPage` does, so they depend on a SHAPE. A test hands them an
7
+ * object literal; an app hands them an adapter over its own HTTP client. No
8
+ * React context, no app-local provider.
9
+ *
10
+ * WHY EVERY NOUN HERE IS SOURCE-AGNOSTIC (bd startsim-768w.38.2, and the rule
11
+ * ratified in feedback_foundry_builder_generic_not_usecase): the record a
12
+ * Granola meeting becomes must be the SAME record a Dropbox file becomes. There
13
+ * is no `GranolaNote`, no `transcript` field, and no `if (source === ...)`
14
+ * anywhere in this package. A vendor difference arrives as DATA — a source key,
15
+ * a source label, a named section — exactly the way the connector catalog
16
+ * already works.
17
+ *
18
+ * THE TEST APPLIED TO EVERY FIELD BELOW: would this still make sense if the only
19
+ * connected source were Dropbox? `sections` passes (a PDF has extracted text the
20
+ * way a meeting has a transcript). A `speakers` field would not have, which is
21
+ * why speakers are `participants` and why they are `PartyRef`s.
22
+ *
23
+ * SCHEMA COUPLING IS DELIBERATELY CONFINED: nothing outside `./entities` knows
24
+ * how a `SourceDocument` is stored. When bd startsim-768w.38.2 lands and the
25
+ * attribute names shift, `./entities.ts` is the one file that changes.
26
+ */
27
+
28
+ /**
29
+ * Which connector filled a record. FREE-FORM on purpose — `granola`, `dropbox`,
30
+ * `gdrive`, `stripe`, `upload`, and whatever ships next week. A union type here
31
+ * would mean a fourth source could not be added without an app change, which is
32
+ * precisely the thing the epic forbids.
33
+ */
34
+ export type SourceKey = string;
35
+
36
+ /**
37
+ * The tenant boundary as the data plane actually enforces it.
38
+ *
39
+ * `orgId` is the JWT `org_id` claim — under customer workspaces (bd
40
+ * startsim-768w.34.4) that is the CUSTOMER's org, not the foundry's root
41
+ * company, and it is the line an answer must never cross.
42
+ *
43
+ * `ownerSub` is the central user_id stamped on every row as `owner_sub`. Today
44
+ * the tenant API scopes reads by org and by readable TYPE, not by owner — so a
45
+ * scope carrying only `orgId` is the honest default. A caller that wants the
46
+ * narrower posture passes `ownerSub` and gets it enforced here as well.
47
+ */
48
+ export interface CorpusScope {
49
+ orgId: string;
50
+ ownerSub?: string;
51
+ }
52
+
53
+ /**
54
+ * A person or company the corpus refers to, as referenced FROM a record.
55
+ * One noun with a subtype, never separate Contact/Customer types
56
+ * (feedback_brutally_generic).
57
+ */
58
+ export interface PartyRef {
59
+ id: string;
60
+ name: string;
61
+ /** `person` | `company` | anything a tenant models. Data, not an enum. */
62
+ kind?: string;
63
+ email?: string;
64
+ }
65
+
66
+ /** A Party as a first-class record. */
67
+ export interface Party extends PartyRef {
68
+ source: SourceKey;
69
+ sourceLabel?: string;
70
+ sourceRef?: string;
71
+ emails?: string[];
72
+ scope: CorpusScope;
73
+ }
74
+
75
+ /**
76
+ * Long-form text a source carried alongside the document's own body.
77
+ *
78
+ * A Granola note contributes `{key:'transcript', label:'Transcript'}`; a Dropbox
79
+ * PDF contributes `{key:'extracted_text', label:'Extracted text'}`; a Stripe
80
+ * invoice contributes none. The composer renders whatever it is handed and names
81
+ * it with the source's own label, so no screen knows what a transcript is.
82
+ */
83
+ export interface DocumentSection {
84
+ key: string;
85
+ label: string;
86
+ text: string;
87
+ }
88
+
89
+ /**
90
+ * The corpus unit. Everything an AI reads is one of these, whatever pulled it in.
91
+ */
92
+ export interface SourceDocument {
93
+ id: string;
94
+ title: string;
95
+ /** The primary readable text — a summary, a description, a body. */
96
+ body: string;
97
+ /** Additional long-form the source provided. See `DocumentSection`. */
98
+ sections: DocumentSection[];
99
+ /** When the thing HAPPENED, which is not when it was ingested. */
100
+ occurredAt: string | null;
101
+ source: SourceKey;
102
+ /** Human name of the source, from the catalog — never hardcoded per vendor. */
103
+ sourceLabel: string;
104
+ /** The vendor's own id, so a row can be traced back out of the system. */
105
+ sourceRef: string;
106
+ /** Deep link into the vendor, when it has one. */
107
+ url?: string;
108
+ participants: PartyRef[];
109
+ scope: CorpusScope;
110
+ /** When the connector last wrote this row. Drives the freshness claim. */
111
+ ingestedAt?: string | null;
112
+ }
113
+
114
+ /**
115
+ * Stripe's side of the corpus, as ONE type discriminated by subtype
116
+ * (`charge` | `invoice` | `subscription` | ...), pointing at a Party.
117
+ * A future billing connector fills the same type.
118
+ */
119
+ export interface MoneyEvent {
120
+ id: string;
121
+ subtype: string;
122
+ /** Minor units, as every payment vendor reports them. */
123
+ amount: number;
124
+ currency: string;
125
+ status: string;
126
+ occurredAt: string | null;
127
+ source: SourceKey;
128
+ sourceLabel: string;
129
+ sourceRef: string;
130
+ url?: string;
131
+ description?: string;
132
+ party: PartyRef | null;
133
+ scope: CorpusScope;
134
+ }
135
+
136
+ /**
137
+ * Freshness is part of the SCREEN, not a settings page (bd startsim-768w.38.6).
138
+ * A corpus silently four days stale is what makes an AI answer confidently
139
+ * wrong, so the Library states it per source and the Ask surface repeats it.
140
+ */
141
+ export interface SourceFreshness {
142
+ source: SourceKey;
143
+ label: string;
144
+ /** How many documents this source has contributed to the corpus. */
145
+ documentCount: number;
146
+ lastSyncedAt: string | null;
147
+ /**
148
+ * `ok` — synced recently and cleanly.
149
+ * `stale` — the last sync succeeded but was long enough ago to matter.
150
+ * `error` — the last sync failed; the corpus is missing whatever came after.
151
+ * `never` — connected but has never delivered a record.
152
+ */
153
+ status: 'ok' | 'stale' | 'error' | 'never';
154
+ /** Why it is not `ok`, in the words the server used. */
155
+ detail?: string;
156
+ }
157
+
158
+ /** DRF PageNumberPagination, which is what the tenant data API speaks. */
159
+ export interface CorpusPage<T> {
160
+ count: number;
161
+ next: string | null;
162
+ previous: string | null;
163
+ results: T[];
164
+ /**
165
+ * Which of the requested filters the SERVER actually applied.
166
+ *
167
+ * A search box wired to a backend that ignores `search` renders an unfiltered
168
+ * list that looks filtered, which is a lie the user has no way to catch. When
169
+ * this is present, the Library states the gap on the screen instead. Omit it
170
+ * and the composer assumes everything requested was honoured.
171
+ */
172
+ appliedFilters?: string[];
173
+ }
174
+
175
+ /**
176
+ * Every filter here is a SERVER parameter (CLAUDE.md rule 8). The Library never
177
+ * loads the corpus and filters it in the browser — that breaks the count, breaks
178
+ * the page, and stops working at exactly the size where it starts to matter.
179
+ */
180
+ export interface DocumentQuery {
181
+ search?: string;
182
+ source?: SourceKey;
183
+ partyId?: string;
184
+ /** ISO instants, filtering on `occurredAt`. */
185
+ occurredAfter?: string;
186
+ occurredBefore?: string;
187
+ page?: number;
188
+ pageSize?: number;
189
+ }
190
+
191
+ /**
192
+ * What the composers are handed. An app implements it over its own HTTP client;
193
+ * `createCorpusApi` in `./client` does that for the tenant `/api/v1/*` endpoints.
194
+ */
195
+ export interface CorpusApi {
196
+ listDocuments(query: DocumentQuery): Promise<CorpusPage<SourceDocument>>;
197
+ getDocument(id: string): Promise<SourceDocument>;
198
+ listParties(query?: { search?: string; page?: number; pageSize?: number }): Promise<CorpusPage<Party>>;
199
+ /** Per-source freshness, for the strip at the top of the Library. */
200
+ listSources(): Promise<SourceFreshness[]>;
201
+ /**
202
+ * Optional because a tenant with no payment connector has no money events, and
203
+ * a corpus surface that requires one would fail the Dropbox-only test.
204
+ */
205
+ listMoneyEvents?(query?: {
206
+ partyId?: string;
207
+ search?: string;
208
+ page?: number;
209
+ pageSize?: number;
210
+ }): Promise<CorpusPage<MoneyEvent>>;
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Ask & Generate (bd startsim-768w.38.7)
215
+ // ---------------------------------------------------------------------------
216
+
217
+ /**
218
+ * ASK answers a question from the corpus. GENERATE produces a draft — a
219
+ * contract, a follow-up, a summary — from a prompt plus the corpus. Same
220
+ * retrieval, same citation rules, one surface; the only difference is what the
221
+ * model is told to emit.
222
+ */
223
+ export type AskMode = 'ask' | 'generate';
224
+
225
+ /** A record the retriever selected, with the reason it selected it. */
226
+ export interface RetrievedDocument {
227
+ document: SourceDocument;
228
+ score: number;
229
+ /** Which query terms hit, so the ranking can be argued with rather than trusted. */
230
+ matchedTerms: string[];
231
+ /** The passage the term matched in, for the citation card. */
232
+ snippet: string;
233
+ }
234
+
235
+ /**
236
+ * A citation the model emitted, AFTER it has been resolved back to a record the
237
+ * retriever actually supplied. A citation that cannot be resolved is dropped —
238
+ * see `AskResult.unresolvedCitations`.
239
+ */
240
+ export interface Citation {
241
+ documentId: string;
242
+ document: SourceDocument;
243
+ /** The model's own quote, for the user to check against the record. */
244
+ quote: string;
245
+ }
246
+
247
+ export interface AskRequest {
248
+ question: string;
249
+ mode: AskMode;
250
+ scope: CorpusScope;
251
+ /** Extra instructions for GENERATE — tone, format, the kind of document. */
252
+ instructions?: string;
253
+ /** Narrow the corpus before retrieval, exactly like the Library's filters. */
254
+ filters?: Pick<DocumentQuery, 'source' | 'partyId' | 'occurredAfter' | 'occurredBefore'>;
255
+ }
256
+
257
+ /**
258
+ * `answered` — an answer with at least one resolved citation.
259
+ * `uncited` — the model produced text but nothing traceable. The UI must SAY so
260
+ * rather than render it: an answer you cannot trace is an answer you cannot
261
+ * trust with a contract.
262
+ * `no_evidence` — retrieval found nothing in scope. This is the correct outcome
263
+ * of a cross-scope question and is not an error.
264
+ */
265
+ export type AskStatus = 'answered' | 'uncited' | 'no_evidence';
266
+
267
+ export interface AskResult {
268
+ status: AskStatus;
269
+ answer: string;
270
+ citations: Citation[];
271
+ /** Everything retrieval offered the model, so "why did it not use X" is answerable. */
272
+ considered: RetrievedDocument[];
273
+ /**
274
+ * Ids the model cited that retrieval never supplied. Non-empty means the model
275
+ * invented a source; it is surfaced, never silently swallowed.
276
+ */
277
+ unresolvedCitations: string[];
278
+ /**
279
+ * How many candidate records were discarded by the scope guard before the
280
+ * prompt was built. A non-zero value on a correctly-scoped API means the API
281
+ * returned something it should not have, and is worth seeing.
282
+ */
283
+ droppedOutOfScope: number;
284
+ /** Which provider/model actually answered, and whether it was a real one. */
285
+ provider: LlmDescription;
286
+ /** Freshness at answer time — an answer over a stale corpus says so. */
287
+ freshness?: SourceFreshness[];
288
+ }
289
+
290
+ export interface LlmDescription {
291
+ provider: string;
292
+ model: string;
293
+ /** False when the answer came from a stub/fixture rather than a live model. */
294
+ live: boolean;
295
+ }
296
+
297
+ /**
298
+ * The document payload handed to the model, with the id it must echo back.
299
+ * ONE call carries all of them (feedback_batch_llm_calls) — never a loop of N.
300
+ */
301
+ export interface PromptDocument {
302
+ id: string;
303
+ title: string;
304
+ source: string;
305
+ occurredAt: string | null;
306
+ participants: string[];
307
+ text: string;
308
+ }
309
+
310
+ export interface AnswerRequest {
311
+ question: string;
312
+ mode: AskMode;
313
+ instructions?: string;
314
+ documents: PromptDocument[];
315
+ }
316
+
317
+ /** The structured shape the model must return. Validated before it is trusted. */
318
+ export interface AnswerDraft {
319
+ answer: string;
320
+ citations: Array<{ documentId: string; quote?: string }>;
321
+ /** The model's own admission that the documents do not answer the question. */
322
+ unanswerable?: boolean;
323
+ }
324
+
325
+ /**
326
+ * The LLM seam. Deliberately ONE method that takes N documents, so the batching
327
+ * rule is structural rather than a thing a caller has to remember.
328
+ *
329
+ * `@startsimpli/llm` is the only implementation that talks to a provider — see
330
+ * `./llm`. Nothing in this package constructs a provider client.
331
+ */
332
+ export interface CorpusLlm {
333
+ answer(input: AnswerRequest): Promise<AnswerDraft>;
334
+ describe(): LlmDescription;
335
+ }
336
+
337
+ /**
338
+ * What `AskPage` is injected with. An app points it at its own route handler;
339
+ * the handler calls `askCorpus` server-side, where the API key lives.
340
+ */
341
+ export interface CorpusAskApi {
342
+ ask(request: AskRequest): Promise<AskResult>;
343
+ /**
344
+ * Persist a generated draft back INTO the corpus, so the draft is part of the
345
+ * corpus next time. Optional: a read-only deployment simply omits it and the
346
+ * Save control does not render.
347
+ */
348
+ saveDraft?(input: {
349
+ title: string;
350
+ body: string;
351
+ citations: string[];
352
+ scope: CorpusScope;
353
+ }): Promise<SourceDocument>;
354
+ }
package/src/index.ts CHANGED
@@ -12,3 +12,12 @@ export * from './types/index';
12
12
  export * from './api/index';
13
13
  export * from './hooks/index';
14
14
  export * from './components/index';
15
+
16
+ /**
17
+ * The corpus layer — the READ side of connections (bd startsim-768w.38.6 and
18
+ * startsim-768w.38.7). Browser-safe by construction: the only module that talks
19
+ * to an LLM provider is `@startsimpli/integrations/corpus-llm`, which is NOT
20
+ * re-exported here, so mounting a composer can never pull a provider key into a
21
+ * client bundle.
22
+ */
23
+ export * from './corpus/index';