{"version":3,"file":"sentence.mjs","names":[],"sources":["../../../../../../../../ai/src/rag/chunk/sentence.ts"],"sourcesContent":["import type { Chunk } from \"../contracts/chunk-options.type\";\n\n/** Matches a sentence terminator (`.`, `!`, `?`) followed by whitespace. */\nconst SENTENCE_BOUNDARY = /([.!?])\\s+/g;\n\n/**\n * Sentence-aware character splitter.\n *\n * Splits the text on sentence terminators (`. `, `! `, `? `), keeping the\n * terminator attached, then greedily packs whole sentences into chunks up\n * to `size` characters, carrying `overlap` characters forward between\n * adjacent chunks. A single sentence longer than `size` becomes its own\n * (oversize) chunk rather than being cut mid-sentence. Spans are exact.\n */\nexport function sentenceChunk(text: string, size: number, overlap: number): Chunk[] {\n  if (text.trim().length === 0) {\n    return [];\n  }\n\n  const sentences = splitSentences(text);\n  const chunks: Chunk[] = [];\n\n  let bufferStart = -1;\n  let bufferEnd = -1;\n  let index = 0;\n\n  const flush = (): void => {\n    if (bufferStart === -1) {\n      return;\n    }\n\n    chunks.push({\n      text: text.slice(bufferStart, bufferEnd),\n      index,\n      span: [bufferStart, bufferEnd],\n    });\n    index += 1;\n  };\n\n  for (const sentence of sentences) {\n    if (bufferStart === -1) {\n      bufferStart = sentence.start;\n      bufferEnd = sentence.end;\n\n      continue;\n    }\n\n    if (sentence.end - bufferStart <= size) {\n      bufferEnd = sentence.end;\n\n      continue;\n    }\n\n    flush();\n\n    const overlapStart =\n      overlap > 0 ? Math.max(bufferStart, sentence.start - overlap) : sentence.start;\n\n    bufferStart = overlapStart;\n    bufferEnd = sentence.end;\n  }\n\n  flush();\n\n  return chunks;\n}\n\n/** A sentence with its absolute `[start, end)` span in the original text. */\ntype SentenceSpan = {\n  start: number;\n  end: number;\n};\n\n/**\n * Split `text` into sentence spans on terminator + whitespace, keeping the\n * terminator with its sentence and absorbing the trailing whitespace into\n * the boundary so reconstructing the spans loses no characters.\n */\nfunction splitSentences(text: string): SentenceSpan[] {\n  const spans: SentenceSpan[] = [];\n  let start = 0;\n  let match: RegExpExecArray | null;\n\n  SENTENCE_BOUNDARY.lastIndex = 0;\n\n  while ((match = SENTENCE_BOUNDARY.exec(text)) !== null) {\n    const end = match.index + match[0].length;\n\n    spans.push({ start, end });\n    start = end;\n  }\n\n  if (start < text.length) {\n    spans.push({ start, end: text.length });\n  }\n\n  return spans;\n}\n"],"mappings":";;AAGA,MAAM,oBAAoB;;;;;;;;;;AAW1B,SAAgB,cAAc,MAAc,MAAc,SAA0B;CAClF,IAAI,KAAK,KAAK,CAAC,CAAC,WAAW,GACzB,OAAO,CAAC;CAGV,MAAM,YAAY,eAAe,IAAI;CACrC,MAAM,SAAkB,CAAC;CAEzB,IAAI,cAAc;CAClB,IAAI,YAAY;CAChB,IAAI,QAAQ;CAEZ,MAAM,cAAoB;EACxB,IAAI,gBAAgB,IAClB;EAGF,OAAO,KAAK;GACV,MAAM,KAAK,MAAM,aAAa,SAAS;GACvC;GACA,MAAM,CAAC,aAAa,SAAS;EAC/B,CAAC;EACD,SAAS;CACX;CAEA,KAAK,MAAM,YAAY,WAAW;EAChC,IAAI,gBAAgB,IAAI;GACtB,cAAc,SAAS;GACvB,YAAY,SAAS;GAErB;EACF;EAEA,IAAI,SAAS,MAAM,eAAe,MAAM;GACtC,YAAY,SAAS;GAErB;EACF;EAEA,MAAM;EAKN,cAFE,UAAU,IAAI,KAAK,IAAI,aAAa,SAAS,QAAQ,OAAO,IAAI,SAAS;EAG3E,YAAY,SAAS;CACvB;CAEA,MAAM;CAEN,OAAO;AACT;;;;;;AAaA,SAAS,eAAe,MAA8B;CACpD,MAAM,QAAwB,CAAC;CAC/B,IAAI,QAAQ;CACZ,IAAI;CAEJ,kBAAkB,YAAY;CAE9B,QAAQ,QAAQ,kBAAkB,KAAK,IAAI,OAAO,MAAM;EACtD,MAAM,MAAM,MAAM,QAAQ,MAAM,EAAE,CAAC;EAEnC,MAAM,KAAK;GAAE;GAAO;EAAI,CAAC;EACzB,QAAQ;CACV;CAEA,IAAI,QAAQ,KAAK,QACf,MAAM,KAAK;EAAE;EAAO,KAAK,KAAK;CAAO,CAAC;CAGxC,OAAO;AACT"}