@startsimpli/integrations 0.2.13 → 0.2.16

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.
@@ -281,3 +281,79 @@ const OUTCOME_HEADING = /\b(next\s*steps?|action\s*items?|follow[\s-]?ups?|to[\s
281
281
  export function outcomeSections(text: string): TextSection[] {
282
282
  return splitSections(text).filter((section) => OUTCOME_HEADING.test(section.heading));
283
283
  }
284
+
285
+ // ---------------------------------------------------------------------------
286
+ // assembling a document's text
287
+ // ---------------------------------------------------------------------------
288
+
289
+ /** The text-bearing shape, so this file still knows nothing about the corpus. */
290
+ interface TextBearing {
291
+ body: string;
292
+ summary?: string;
293
+ sections: Array<{ label: string; text: string }>;
294
+ }
295
+
296
+ /**
297
+ * Every distinct piece of text a document carries, summary FIRST.
298
+ *
299
+ * DE-DUPLICATED, because `body` deliberately falls back to `summary` on a corpus
300
+ * that has never seen a full text — and sending the same 5,000 characters to a
301
+ * model twice is a bill, not a feature.
302
+ *
303
+ * SUMMARY FIRST is not cosmetic. It is the piece a person already curated, the
304
+ * piece the deterministic pass reads, and the piece that must survive when the
305
+ * rest has to be cut.
306
+ */
307
+ export function documentParts(document: TextBearing): string[] {
308
+ const summary = (document.summary ?? '').trim();
309
+ const body = (document.body ?? '').trim();
310
+ const parts = [summary, body === summary ? '' : body];
311
+ for (const section of document.sections ?? []) {
312
+ const text = (section.text ?? '').trim();
313
+ if (!text || parts.includes(text)) continue;
314
+ parts.push(`## ${section.label}\n${text}`);
315
+ }
316
+ return parts.filter(Boolean);
317
+ }
318
+
319
+ /**
320
+ * Trim to a budget while KEEPING THE END.
321
+ *
322
+ * A prefix `slice(0, maxChars)` is the wrong cut for a conversation. People
323
+ * agree things in the last five minutes of a meeting — "I'll send it Friday",
324
+ * "put that on Megan" — so a head-first truncation deletes precisely the half
325
+ * the extractor exists to read, and does it silently. Keeping a quarter from the
326
+ * front preserves who is in the room and what it is about; the rest of the
327
+ * budget goes to the end.
328
+ */
329
+ export function trimKeepingTheEnd(text: string, maxChars: number): string {
330
+ if (maxChars <= 0) return '';
331
+ if (text.length <= maxChars) return text;
332
+ const head = Math.floor(maxChars * 0.25);
333
+ const tail = maxChars - head;
334
+ return [
335
+ text.slice(0, head),
336
+ `…[truncated ${text.length - maxChars} characters]…`,
337
+ text.slice(text.length - tail),
338
+ ].join('\n');
339
+ }
340
+
341
+ /**
342
+ * A document as one string for a prompt, inside a character budget.
343
+ *
344
+ * THE SUMMARY IS PAID FOR FIRST and only trimmed if it alone does not fit. Once
345
+ * a transcript lands, `body` is ten times the size of the summary; a naive join
346
+ * plus a prefix cut spends the entire budget on the opening minutes of the
347
+ * meeting and never reaches the outcome block at all — on the Ask surface, whose
348
+ * per-document budget is 6,000 characters, the summary would simply stop
349
+ * reaching the model.
350
+ */
351
+ export function documentText(document: TextBearing, maxChars: number): string {
352
+ const [first = '', ...rest] = documentParts(document);
353
+ const summary = trimKeepingTheEnd(first, maxChars);
354
+ const verbatim = rest.join('\n\n');
355
+ if (!verbatim) return summary;
356
+ const remaining = maxChars - summary.length - 2;
357
+ const kept = trimKeepingTheEnd(verbatim, remaining);
358
+ return [summary, kept].filter(Boolean).join('\n\n');
359
+ }
@@ -92,8 +92,26 @@ export interface DocumentSection {
92
92
  export interface SourceDocument {
93
93
  id: string;
94
94
  title: string;
95
- /** The primary readable text — a summary, a description, a body. */
95
+ /**
96
+ * The FULL text — a transcript, a PDF's extracted text, a document's contents.
97
+ *
98
+ * Falls back to `summary` when the source has never delivered a full text, so
99
+ * that every consumer of `body` keeps working on a corpus of summaries. When
100
+ * both exist they are DIFFERENT strings and both survive; see `summary`.
101
+ */
96
102
  body: string;
103
+ /**
104
+ * The short form somebody or something already WROTE — Granola's AI summary, a
105
+ * file's description. Two fields and not one, exactly as the tenant's canonical
106
+ * schema declares them, because the day a transcript arrives is the day a
107
+ * single field would silently lose the summary.
108
+ *
109
+ * THAT IS NOT HYPOTHETICAL. This mapper collapsed them for the whole of the
110
+ * epic so far, and the collapse would have deleted the deterministic rules pass
111
+ * — the `## Next Steps` block lives in the summary and nowhere else — on the
112
+ * exact deploy that first switched transcripts on.
113
+ */
114
+ summary?: string;
97
115
  /** Additional long-form the source provided. See `DocumentSection`. */
98
116
  sections: DocumentSection[];
99
117
  /** When the thing HAPPENED, which is not when it was ingested. */
@@ -109,6 +127,19 @@ export interface SourceDocument {
109
127
  scope: CorpusScope;
110
128
  /** When the connector last wrote this row. Drives the freshness claim. */
111
129
  ingestedAt?: string | null;
130
+ /**
131
+ * Attribute names the SERVER withheld from this reader
132
+ * (bd startsim-768w.38.33). The tenant's per-field read gate strips them from
133
+ * `data` and names them back in `redacted_fields`; this carries that fact all
134
+ * the way to the screen and into the prompt.
135
+ *
136
+ * IT HAS TO TRAVEL, and that is the whole reason it is on the document rather
137
+ * than swallowed at the mapper. A record whose transcript was withheld is NOT
138
+ * a record with no transcript. Anything that cannot tell the two apart — a
139
+ * reader, or a model asked "was this discussed?" — answers "no" with total
140
+ * confidence about text it was simply never shown.
141
+ */
142
+ withheldFields?: string[];
112
143
  }
113
144
 
114
145
  /**
@@ -281,6 +312,14 @@ export interface AskResult {
281
312
  * returned something it should not have, and is worth seeing.
282
313
  */
283
314
  droppedOutOfScope: number;
315
+ /**
316
+ * Attribute names the server withheld from this reader on at least one record
317
+ * that was considered (bd startsim-768w.38.33). Reported for exactly the
318
+ * reason `droppedOutOfScope` is: an answer built from a partial record has to
319
+ * say so, or "your corpus has nothing about that" and "you were not shown the
320
+ * part that does" become the same sentence.
321
+ */
322
+ withheldFields?: string[];
284
323
  /** Which provider/model actually answered, and whether it was a real one. */
285
324
  provider: LlmDescription;
286
325
  /** Freshness at answer time — an answer over a stale corpus says so. */
@@ -305,6 +344,13 @@ export interface PromptDocument {
305
344
  occurredAt: string | null;
306
345
  participants: string[];
307
346
  text: string;
347
+ /**
348
+ * Fields the reader was not allowed to see (bd startsim-768w.38.33). Rendered
349
+ * into the document block so the model states a limit instead of inventing a
350
+ * certainty: without it, a summary-only view of a meeting whose transcript was
351
+ * withheld reads as proof the thing was never discussed.
352
+ */
353
+ withheldFields?: string[];
308
354
  }
309
355
 
310
356
  export interface AnswerRequest {
@@ -515,9 +561,56 @@ export interface CommitmentApi {
515
561
  */
516
562
  export interface CorpusExtractorLlm {
517
563
  extract(input: ExtractionRequest): Promise<ExtractionDraft>;
564
+ /**
565
+ * CONFIRM the model's own candidates against the records they came from.
566
+ *
567
+ * OPTIONAL, and the run reports `verified: false` when it is missing rather
568
+ * than pretending a check happened. Present on the shipped adapter.
569
+ *
570
+ * WHY IT EXISTS. More text raises recall and lowers precision, and transcripts
571
+ * are roughly ten times the text of a summary. Measured on the summaries
572
+ * alone, adding the model pass took recall 56.1% → 65.9% and precision 95.8% →
573
+ * 67.5%; a corpus of verbatim speech makes that trade worse, not better,
574
+ * because a transcript is full of sentences that SOUND like commitments and
575
+ * are hedges, jokes, or somebody reading an agenda aloud.
576
+ *
577
+ * ONE BATCHED CALL over every candidate with echoed ids
578
+ * (feedback_batch_llm_calls) — never a call per item.
579
+ */
580
+ verify?(input: VerificationRequest): Promise<VerificationDraft>;
518
581
  describe(): LlmDescription;
519
582
  }
520
583
 
584
+ /**
585
+ * What the verifier is asked. The candidates AND the records they claim to come
586
+ * from, so the check is against the source rather than against the candidate's
587
+ * own plausibility.
588
+ */
589
+ export interface VerificationRequest {
590
+ documents: Array<{ id: string; title: string; text: string }>;
591
+ candidates: Array<{
592
+ /** Echoed back on the verdict. Stable only within this call. */
593
+ id: string;
594
+ documentId: string;
595
+ subtype: string;
596
+ title: string;
597
+ owner?: string;
598
+ /** The span the extractor said this came from. */
599
+ quote: string;
600
+ }>;
601
+ }
602
+
603
+ export interface VerificationDraft {
604
+ verdicts: Array<{
605
+ id: string;
606
+ confirmed: boolean;
607
+ /** The supporting span, which the verifier may correct. */
608
+ quote?: string;
609
+ /** Why not, in the verifier's words. Surfaced, never silently swallowed. */
610
+ reason?: string;
611
+ }>;
612
+ }
613
+
521
614
  export interface ExtractionRequest {
522
615
  documents: Array<{
523
616
  id: string;
@@ -591,8 +684,21 @@ export interface ExtractionResult {
591
684
  nextOffset: number | null;
592
685
  /** Items the vendor's own structure produced, with no model involved. */
593
686
  fromRules: number;
594
- /** Items only the model found. */
687
+ /** Items only the model found, AFTER verification. */
595
688
  fromModel: number;
689
+ /**
690
+ * Model candidates thrown away because the span they quoted does not appear in
691
+ * the record. Free to check, and it is the signature of an invented item.
692
+ */
693
+ ungrounded: number;
694
+ /** Grounded candidates the verification pass declined to confirm. */
695
+ unconfirmed: number;
696
+ /**
697
+ * Whether the model verification pass actually ran. False means the surviving
698
+ * model items passed the deterministic grounding check and nothing else — the
699
+ * number is still reported, it is just not called verified.
700
+ */
701
+ verified: boolean;
596
702
  created: number;
597
703
  /** Existing items a new mention was attached to — the dedupe, counted. */
598
704
  merged: number;