@startsimpli/integrations 0.2.11 → 0.2.14

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. */
@@ -515,9 +533,56 @@ export interface CommitmentApi {
515
533
  */
516
534
  export interface CorpusExtractorLlm {
517
535
  extract(input: ExtractionRequest): Promise<ExtractionDraft>;
536
+ /**
537
+ * CONFIRM the model's own candidates against the records they came from.
538
+ *
539
+ * OPTIONAL, and the run reports `verified: false` when it is missing rather
540
+ * than pretending a check happened. Present on the shipped adapter.
541
+ *
542
+ * WHY IT EXISTS. More text raises recall and lowers precision, and transcripts
543
+ * are roughly ten times the text of a summary. Measured on the summaries
544
+ * alone, adding the model pass took recall 56.1% → 65.9% and precision 95.8% →
545
+ * 67.5%; a corpus of verbatim speech makes that trade worse, not better,
546
+ * because a transcript is full of sentences that SOUND like commitments and
547
+ * are hedges, jokes, or somebody reading an agenda aloud.
548
+ *
549
+ * ONE BATCHED CALL over every candidate with echoed ids
550
+ * (feedback_batch_llm_calls) — never a call per item.
551
+ */
552
+ verify?(input: VerificationRequest): Promise<VerificationDraft>;
518
553
  describe(): LlmDescription;
519
554
  }
520
555
 
556
+ /**
557
+ * What the verifier is asked. The candidates AND the records they claim to come
558
+ * from, so the check is against the source rather than against the candidate's
559
+ * own plausibility.
560
+ */
561
+ export interface VerificationRequest {
562
+ documents: Array<{ id: string; title: string; text: string }>;
563
+ candidates: Array<{
564
+ /** Echoed back on the verdict. Stable only within this call. */
565
+ id: string;
566
+ documentId: string;
567
+ subtype: string;
568
+ title: string;
569
+ owner?: string;
570
+ /** The span the extractor said this came from. */
571
+ quote: string;
572
+ }>;
573
+ }
574
+
575
+ export interface VerificationDraft {
576
+ verdicts: Array<{
577
+ id: string;
578
+ confirmed: boolean;
579
+ /** The supporting span, which the verifier may correct. */
580
+ quote?: string;
581
+ /** Why not, in the verifier's words. Surfaced, never silently swallowed. */
582
+ reason?: string;
583
+ }>;
584
+ }
585
+
521
586
  export interface ExtractionRequest {
522
587
  documents: Array<{
523
588
  id: string;
@@ -591,8 +656,21 @@ export interface ExtractionResult {
591
656
  nextOffset: number | null;
592
657
  /** Items the vendor's own structure produced, with no model involved. */
593
658
  fromRules: number;
594
- /** Items only the model found. */
659
+ /** Items only the model found, AFTER verification. */
595
660
  fromModel: number;
661
+ /**
662
+ * Model candidates thrown away because the span they quoted does not appear in
663
+ * the record. Free to check, and it is the signature of an invented item.
664
+ */
665
+ ungrounded: number;
666
+ /** Grounded candidates the verification pass declined to confirm. */
667
+ unconfirmed: number;
668
+ /**
669
+ * Whether the model verification pass actually ran. False means the surviving
670
+ * model items passed the deterministic grounding check and nothing else — the
671
+ * number is still reported, it is just not called verified.
672
+ */
673
+ verified: boolean;
596
674
  created: number;
597
675
  /** Existing items a new mention was attached to — the dedupe, counted. */
598
676
  merged: number;