agentfootprint 8.9.0 → 8.10.0

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.
Files changed (151) hide show
  1. package/AGENTS.md +2 -0
  2. package/CLAUDE.md +4 -0
  3. package/bin/agentfootprint-index.mjs +192 -0
  4. package/dist/doors/rag.js +66 -0
  5. package/dist/doors/rag.js.map +1 -0
  6. package/dist/esm/doors/rag.d.ts +49 -0
  7. package/dist/esm/doors/rag.js +50 -0
  8. package/dist/esm/doors/rag.js.map +1 -0
  9. package/dist/esm/rag/hash.d.ts +2 -0
  10. package/dist/esm/rag/hash.js +22 -0
  11. package/dist/esm/rag/hash.js.map +1 -0
  12. package/dist/esm/rag/index.d.ts +17 -0
  13. package/dist/esm/rag/index.js +17 -0
  14. package/dist/esm/rag/index.js.map +1 -0
  15. package/dist/esm/rag/indexCorpus.d.ts +102 -0
  16. package/dist/esm/rag/indexCorpus.js +339 -0
  17. package/dist/esm/rag/indexCorpus.js.map +1 -0
  18. package/dist/esm/rag/indexFolder.d.ts +40 -0
  19. package/dist/esm/rag/indexFolder.js +42 -0
  20. package/dist/esm/rag/indexFolder.js.map +1 -0
  21. package/dist/esm/rag/loadDocuments.d.ts +34 -0
  22. package/dist/esm/rag/loadDocuments.js +160 -0
  23. package/dist/esm/rag/loadDocuments.js.map +1 -0
  24. package/dist/esm/rag/loaders/html.d.ts +42 -0
  25. package/dist/esm/rag/loaders/html.js +48 -0
  26. package/dist/esm/rag/loaders/html.js.map +1 -0
  27. package/dist/esm/rag/loaders/index.d.ts +30 -0
  28. package/dist/esm/rag/loaders/index.js +24 -0
  29. package/dist/esm/rag/loaders/index.js.map +1 -0
  30. package/dist/esm/rag/loaders/markdown.d.ts +27 -0
  31. package/dist/esm/rag/loaders/markdown.js +12 -0
  32. package/dist/esm/rag/loaders/markdown.js.map +1 -0
  33. package/dist/esm/rag/loaders/mock.d.ts +28 -0
  34. package/dist/esm/rag/loaders/mock.js +14 -0
  35. package/dist/esm/rag/loaders/mock.js.map +1 -0
  36. package/dist/esm/rag/loaders/pdf.d.ts +39 -0
  37. package/dist/esm/rag/loaders/pdf.js +90 -0
  38. package/dist/esm/rag/loaders/pdf.js.map +1 -0
  39. package/dist/esm/rag/loaders/text.d.ts +23 -0
  40. package/dist/esm/rag/loaders/text.js +21 -0
  41. package/dist/esm/rag/loaders/text.js.map +1 -0
  42. package/dist/esm/rag/splitDocuments.d.ts +21 -0
  43. package/dist/esm/rag/splitDocuments.js +65 -0
  44. package/dist/esm/rag/splitDocuments.js.map +1 -0
  45. package/dist/esm/rag/splitters/byHeading.d.ts +36 -0
  46. package/dist/esm/rag/splitters/byHeading.js +66 -0
  47. package/dist/esm/rag/splitters/byHeading.js.map +1 -0
  48. package/dist/esm/rag/splitters/byParagraph.d.ts +24 -0
  49. package/dist/esm/rag/splitters/byParagraph.js +16 -0
  50. package/dist/esm/rag/splitters/byParagraph.js.map +1 -0
  51. package/dist/esm/rag/splitters/constants.d.ts +19 -0
  52. package/dist/esm/rag/splitters/constants.js +20 -0
  53. package/dist/esm/rag/splitters/constants.js.map +1 -0
  54. package/dist/esm/rag/splitters/fixedWithOverlap.d.ts +31 -0
  55. package/dist/esm/rag/splitters/fixedWithOverlap.js +34 -0
  56. package/dist/esm/rag/splitters/fixedWithOverlap.js.map +1 -0
  57. package/dist/esm/rag/splitters/index.d.ts +50 -0
  58. package/dist/esm/rag/splitters/index.js +51 -0
  59. package/dist/esm/rag/splitters/index.js.map +1 -0
  60. package/dist/esm/rag/splitters/shared.d.ts +87 -0
  61. package/dist/esm/rag/splitters/shared.js +188 -0
  62. package/dist/esm/rag/splitters/shared.js.map +1 -0
  63. package/dist/esm/rag/splitters/wholeDocument.d.ts +16 -0
  64. package/dist/esm/rag/splitters/wholeDocument.js +10 -0
  65. package/dist/esm/rag/splitters/wholeDocument.js.map +1 -0
  66. package/dist/esm/rag/types.d.ts +200 -0
  67. package/dist/esm/rag/types.js +20 -0
  68. package/dist/esm/rag/types.js.map +1 -0
  69. package/dist/rag/hash.js +26 -0
  70. package/dist/rag/hash.js.map +1 -0
  71. package/dist/rag/index.js +40 -0
  72. package/dist/rag/index.js.map +1 -0
  73. package/dist/rag/indexCorpus.js +344 -0
  74. package/dist/rag/indexCorpus.js.map +1 -0
  75. package/dist/rag/indexFolder.js +46 -0
  76. package/dist/rag/indexFolder.js.map +1 -0
  77. package/dist/rag/loadDocuments.js +164 -0
  78. package/dist/rag/loadDocuments.js.map +1 -0
  79. package/dist/rag/loaders/html.js +53 -0
  80. package/dist/rag/loaders/html.js.map +1 -0
  81. package/dist/rag/loaders/index.js +33 -0
  82. package/dist/rag/loaders/index.js.map +1 -0
  83. package/dist/rag/loaders/markdown.js +16 -0
  84. package/dist/rag/loaders/markdown.js.map +1 -0
  85. package/dist/rag/loaders/mock.js +18 -0
  86. package/dist/rag/loaders/mock.js.map +1 -0
  87. package/dist/rag/loaders/pdf.js +118 -0
  88. package/dist/rag/loaders/pdf.js.map +1 -0
  89. package/dist/rag/loaders/text.js +26 -0
  90. package/dist/rag/loaders/text.js.map +1 -0
  91. package/dist/rag/splitDocuments.js +69 -0
  92. package/dist/rag/splitDocuments.js.map +1 -0
  93. package/dist/rag/splitters/byHeading.js +70 -0
  94. package/dist/rag/splitters/byHeading.js.map +1 -0
  95. package/dist/rag/splitters/byParagraph.js +20 -0
  96. package/dist/rag/splitters/byParagraph.js.map +1 -0
  97. package/dist/rag/splitters/constants.js +23 -0
  98. package/dist/rag/splitters/constants.js.map +1 -0
  99. package/dist/rag/splitters/fixedWithOverlap.js +38 -0
  100. package/dist/rag/splitters/fixedWithOverlap.js.map +1 -0
  101. package/dist/rag/splitters/index.js +60 -0
  102. package/dist/rag/splitters/index.js.map +1 -0
  103. package/dist/rag/splitters/shared.js +199 -0
  104. package/dist/rag/splitters/shared.js.map +1 -0
  105. package/dist/rag/splitters/wholeDocument.js +14 -0
  106. package/dist/rag/splitters/wholeDocument.js.map +1 -0
  107. package/dist/rag/types.js +21 -0
  108. package/dist/rag/types.js.map +1 -0
  109. package/dist/types/doors/rag.d.ts +50 -0
  110. package/dist/types/doors/rag.d.ts.map +1 -0
  111. package/dist/types/rag/hash.d.ts +3 -0
  112. package/dist/types/rag/hash.d.ts.map +1 -0
  113. package/dist/types/rag/index.d.ts +18 -0
  114. package/dist/types/rag/index.d.ts.map +1 -0
  115. package/dist/types/rag/indexCorpus.d.ts +103 -0
  116. package/dist/types/rag/indexCorpus.d.ts.map +1 -0
  117. package/dist/types/rag/indexFolder.d.ts +41 -0
  118. package/dist/types/rag/indexFolder.d.ts.map +1 -0
  119. package/dist/types/rag/loadDocuments.d.ts +35 -0
  120. package/dist/types/rag/loadDocuments.d.ts.map +1 -0
  121. package/dist/types/rag/loaders/html.d.ts +43 -0
  122. package/dist/types/rag/loaders/html.d.ts.map +1 -0
  123. package/dist/types/rag/loaders/index.d.ts +31 -0
  124. package/dist/types/rag/loaders/index.d.ts.map +1 -0
  125. package/dist/types/rag/loaders/markdown.d.ts +28 -0
  126. package/dist/types/rag/loaders/markdown.d.ts.map +1 -0
  127. package/dist/types/rag/loaders/mock.d.ts +29 -0
  128. package/dist/types/rag/loaders/mock.d.ts.map +1 -0
  129. package/dist/types/rag/loaders/pdf.d.ts +40 -0
  130. package/dist/types/rag/loaders/pdf.d.ts.map +1 -0
  131. package/dist/types/rag/loaders/text.d.ts +24 -0
  132. package/dist/types/rag/loaders/text.d.ts.map +1 -0
  133. package/dist/types/rag/splitDocuments.d.ts +22 -0
  134. package/dist/types/rag/splitDocuments.d.ts.map +1 -0
  135. package/dist/types/rag/splitters/byHeading.d.ts +37 -0
  136. package/dist/types/rag/splitters/byHeading.d.ts.map +1 -0
  137. package/dist/types/rag/splitters/byParagraph.d.ts +25 -0
  138. package/dist/types/rag/splitters/byParagraph.d.ts.map +1 -0
  139. package/dist/types/rag/splitters/constants.d.ts +20 -0
  140. package/dist/types/rag/splitters/constants.d.ts.map +1 -0
  141. package/dist/types/rag/splitters/fixedWithOverlap.d.ts +32 -0
  142. package/dist/types/rag/splitters/fixedWithOverlap.d.ts.map +1 -0
  143. package/dist/types/rag/splitters/index.d.ts +51 -0
  144. package/dist/types/rag/splitters/index.d.ts.map +1 -0
  145. package/dist/types/rag/splitters/shared.d.ts +88 -0
  146. package/dist/types/rag/splitters/shared.d.ts.map +1 -0
  147. package/dist/types/rag/splitters/wholeDocument.d.ts +17 -0
  148. package/dist/types/rag/splitters/wholeDocument.d.ts.map +1 -0
  149. package/dist/types/rag/types.d.ts +201 -0
  150. package/dist/types/rag/types.d.ts.map +1 -0
  151. package/package.json +23 -4
@@ -0,0 +1,42 @@
1
+ /**
2
+ * htmlLoader — HTML with the tags taken out, without a parser.
3
+ *
4
+ * Pattern: Adapter behind `DocumentLoader`.
5
+ * Role: rag/ layer. Zero dependency.
6
+ * Emits: N/A.
7
+ *
8
+ * ── What this is, and what it is not ────────────────────────────────────────
9
+ * This is a **tag stripper**, not an HTML parser, and the distinction is worth
10
+ * being blunt about because the failure mode is silent. It removes `<script>`
11
+ * and `<style>` bodies, replaces tags with whitespace, and decodes the handful
12
+ * of entities that appear in real prose. That is enough for documentation
13
+ * pages, exported articles, and saved knowledge-base entries — the things
14
+ * people actually put in a corpus.
15
+ *
16
+ * It is NOT enough for a modern application page. A single-page app's HTML is
17
+ * mostly scaffolding, and what you get back will be navigation labels and
18
+ * button text. If your corpus is that, run a real extractor (Readability,
19
+ * Mercury, your CMS's own export) and feed the result in as text — the
20
+ * `{ text, uri }` arm of `DocumentSource` exists for exactly that.
21
+ *
22
+ * A parser was considered and rejected for v1: every option is a dependency
23
+ * measured in megabytes, and the difference it buys on the documents people
24
+ * index is small. If that stops being true, the fix is another
25
+ * `DocumentLoader` passed ahead of this one — no change here.
26
+ *
27
+ * ── Offsets ─────────────────────────────────────────────────────────────────
28
+ * Tags are replaced with whitespace of the SAME LENGTH rather than deleted, so
29
+ * offsets into the extracted text line up with offsets into the original file.
30
+ * A chunk's `charStart` therefore points at the same place in both, and the
31
+ * text that gets stored is the extracted text, which is what the offsets index.
32
+ */
33
+ import type { DocumentLoader } from '../types.js';
34
+ /**
35
+ * Strip markup to text.
36
+ *
37
+ * Exported because the splitter tests and the security tests both need to
38
+ * assert on it directly, and because a consumer writing their own loader for a
39
+ * markup-ish format may reasonably want the same pass.
40
+ */
41
+ export declare function stripTags(html: string): string;
42
+ export declare function htmlLoader(): DocumentLoader;
@@ -0,0 +1,48 @@
1
+ import { decodeText } from './text.js';
2
+ /** Blank a matched region, preserving newlines so line structure survives. */
3
+ function blank(match) {
4
+ return match.replace(/[^\n]/g, ' ');
5
+ }
6
+ const ENTITIES = {
7
+ '&amp;': '&',
8
+ '&lt;': '<',
9
+ '&gt;': '>',
10
+ '&quot;': '"',
11
+ '&#39;': "'",
12
+ '&apos;': "'",
13
+ '&nbsp;': ' ',
14
+ };
15
+ /**
16
+ * Strip markup to text.
17
+ *
18
+ * Exported because the splitter tests and the security tests both need to
19
+ * assert on it directly, and because a consumer writing their own loader for a
20
+ * markup-ish format may reasonably want the same pass.
21
+ */
22
+ export function stripTags(html) {
23
+ let out = html;
24
+ // Script and style CONTENT is not prose. Blanked wholesale, bodies included.
25
+ out = out.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, blank);
26
+ out = out.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, blank);
27
+ out = out.replace(/<!--[\s\S]*?-->/g, blank);
28
+ // Every remaining tag becomes whitespace of equal length — see the header.
29
+ out = out.replace(/<[^>]*>/g, blank);
30
+ // Entities are decoded LAST and only for the fixed set above. A general
31
+ // numeric decode would let `&#60;script&#62;` reappear as a tag in text that
32
+ // has already been stripped.
33
+ for (const [entity, char] of Object.entries(ENTITIES)) {
34
+ out = out.split(entity).join(char.padEnd(entity.length, ' '));
35
+ }
36
+ return out;
37
+ }
38
+ export function htmlLoader() {
39
+ return {
40
+ name: 'html',
41
+ extensions: ['.html', '.htm', '.xhtml'],
42
+ // eslint-disable-next-line @typescript-eslint/require-await
43
+ async load(input) {
44
+ return { text: stripTags(decodeText(input.bytes)) };
45
+ },
46
+ };
47
+ }
48
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../../../../src/rag/loaders/html.ts"],"names":[],"mappings":"AAiCA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,8EAA8E;AAC9E,SAAS,KAAK,CAAC,KAAa;IAC1B,OAAO,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,QAAQ,GAAqC;IACjD,OAAO,EAAE,GAAG;IACZ,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,GAAG;IACX,QAAQ,EAAE,GAAG;IACb,OAAO,EAAE,GAAG;IACZ,QAAQ,EAAE,GAAG;IACb,QAAQ,EAAE,GAAG;CACd,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,6EAA6E;IAC7E,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,wCAAwC,EAAE,KAAK,CAAC,CAAC;IACnE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,sCAAsC,EAAE,KAAK,CAAC,CAAC;IACjE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;IAC7C,2EAA2E;IAC3E,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACrC,wEAAwE;IACxE,6EAA6E;IAC7E,6BAA6B;IAC7B,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtD,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC;QACvC,4DAA4D;QAC5D,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACtD,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * rag/loaders — one adapter per format, each doing exactly one job: turn
3
+ * bytes into text plus the positions that text came from.
4
+ *
5
+ * Pattern: Adapter (one per format) behind the `DocumentLoader` port.
6
+ * Role: rag/ layer, outermost ring. Three of the five need no dependency
7
+ * at all; only PDF does, and it lazy-loads a single optional peer so
8
+ * importing this barrel costs nothing for a consumer indexing
9
+ * Markdown.
10
+ * Emits: N/A — a loader reads a file and returns text.
11
+ *
12
+ * `DEFAULT_LOADERS` is the routing table `loadDocuments` uses when you pass
13
+ * none. A loader you pass yourself is consulted FIRST, so overriding one is
14
+ * putting yours in front rather than editing a registry.
15
+ */
16
+ import type { DocumentLoader } from '../types.js';
17
+ export { textLoader } from './text.js';
18
+ export { markdownLoader } from './markdown.js';
19
+ export { htmlLoader } from './html.js';
20
+ export { pdfLoader, MissingPdfSupportError, type PdfLoaderOptions } from './pdf.js';
21
+ export { mockLoader, type MockLoaderOptions } from './mock.js';
22
+ /**
23
+ * The loaders `loadDocuments` uses when the caller names none, in routing
24
+ * order.
25
+ *
26
+ * `pdfLoader()` is in the list, but constructing it costs nothing — the
27
+ * optional peer is loaded on the first PDF it is actually asked to read, so a
28
+ * corpus of Markdown never touches it and never needs it installed.
29
+ */
30
+ export declare const DEFAULT_LOADERS: readonly DocumentLoader[];
@@ -0,0 +1,24 @@
1
+ import { textLoader } from './text.js';
2
+ import { markdownLoader } from './markdown.js';
3
+ import { htmlLoader } from './html.js';
4
+ import { pdfLoader } from './pdf.js';
5
+ export { textLoader } from './text.js';
6
+ export { markdownLoader } from './markdown.js';
7
+ export { htmlLoader } from './html.js';
8
+ export { pdfLoader, MissingPdfSupportError } from './pdf.js';
9
+ export { mockLoader } from './mock.js';
10
+ /**
11
+ * The loaders `loadDocuments` uses when the caller names none, in routing
12
+ * order.
13
+ *
14
+ * `pdfLoader()` is in the list, but constructing it costs nothing — the
15
+ * optional peer is loaded on the first PDF it is actually asked to read, so a
16
+ * corpus of Markdown never touches it and never needs it installed.
17
+ */
18
+ export const DEFAULT_LOADERS = Object.freeze([
19
+ markdownLoader(),
20
+ textLoader(),
21
+ htmlLoader(),
22
+ pdfLoader(),
23
+ ]);
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/rag/loaders/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAyB,MAAM,UAAU,CAAC;AACpF,OAAO,EAAE,UAAU,EAA0B,MAAM,WAAW,CAAC;AAE/D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAA8B,MAAM,CAAC,MAAM,CAAC;IACtE,cAAc,EAAE;IAChB,UAAU,EAAE;IACZ,UAAU,EAAE;IACZ,SAAS,EAAE;CACZ,CAAC,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * markdownLoader — Markdown, kept as Markdown.
3
+ *
4
+ * Pattern: Adapter behind `DocumentLoader`.
5
+ * Role: rag/ layer. Zero dependency, and it deliberately does NOT parse.
6
+ * Emits: N/A.
7
+ *
8
+ * ── Why the markup stays ────────────────────────────────────────────────────
9
+ * The obvious thing to do here is strip the syntax and hand over prose. It is
10
+ * the wrong thing, twice over:
11
+ *
12
+ * 1. **The headings are the document's own structure.** `byHeading` cuts on
13
+ * `#` lines, which is the one splitting rule in this library that is not a
14
+ * heuristic — the author already told us where the sections are. Stripping
15
+ * `#` throws that away and leaves us guessing at boundaries we were handed.
16
+ * 2. **Stripping shifts every offset.** `text.slice(charStart, charEnd)` has to
17
+ * equal the chunk, against the text that was stored. A loader that rewrites
18
+ * text and then reports offsets into the original is reporting fiction.
19
+ *
20
+ * Models read Markdown perfectly well; a table that survives as a table is
21
+ * more useful in a prompt than the same table flattened into a word salad.
22
+ *
23
+ * What it DOES do is the same normalisation `textLoader` does, for the same
24
+ * reason: line endings, before any offset exists.
25
+ */
26
+ import type { DocumentLoader } from '../types.js';
27
+ export declare function markdownLoader(): DocumentLoader;
@@ -0,0 +1,12 @@
1
+ import { decodeText } from './text.js';
2
+ export function markdownLoader() {
3
+ return {
4
+ name: 'markdown',
5
+ extensions: ['.md', '.markdown', '.mdx'],
6
+ // eslint-disable-next-line @typescript-eslint/require-await
7
+ async load(input) {
8
+ return { text: decodeText(input.bytes) };
9
+ },
10
+ };
11
+ }
12
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../../../src/rag/loaders/markdown.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,UAAU,cAAc;IAC5B,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,UAAU,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC;QACxC,4DAA4D;QAC5D,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * mockLoader — a loader with no filesystem and no format, for tests.
3
+ *
4
+ * Pattern: Adapter behind `DocumentLoader`.
5
+ * Role: rag/ layer. The first rung of the mock-first ladder for indexing:
6
+ * prove the pipeline runs — discover, split, plan, embed, store,
7
+ * report — before a single real file or a single real embedding is
8
+ * involved.
9
+ * Emits: N/A.
10
+ *
11
+ * It claims whatever extensions you give it (default: everything a test is
12
+ * likely to name), and returns the text you handed it. Unlike the other
13
+ * loaders it never touches bytes at all, which is exactly what makes it
14
+ * useful: a test can assert on chunking and planning without a temp directory.
15
+ */
16
+ import type { DocumentLoader } from '../types.js';
17
+ export interface MockLoaderOptions {
18
+ /** Extensions to claim. Default: a broad set, so routing is never the thing under test. */
19
+ readonly extensions?: readonly string[];
20
+ /**
21
+ * Return text for a uri, instead of decoding the bytes. Use for a fixture
22
+ * corpus that exists only in the test file.
23
+ */
24
+ readonly textFor?: (uri: string) => string | undefined;
25
+ /** Pretend this uri is paginated — for exercising page provenance without a PDF. */
26
+ readonly pagesFor?: (uri: string) => readonly string[] | undefined;
27
+ }
28
+ export declare function mockLoader(options?: MockLoaderOptions): DocumentLoader;
@@ -0,0 +1,14 @@
1
+ import { decodeText } from './text.js';
2
+ export function mockLoader(options = {}) {
3
+ return {
4
+ name: 'mock',
5
+ extensions: options.extensions ?? ['.md', '.txt', '.html', '.pdf', '.mock'],
6
+ // eslint-disable-next-line @typescript-eslint/require-await
7
+ async load(input) {
8
+ const pages = options.pagesFor?.(input.uri);
9
+ const text = options.textFor?.(input.uri) ?? decodeText(input.bytes);
10
+ return { text: pages ? pages.join('\n\n') : text, ...(pages && { pages }) };
11
+ },
12
+ };
13
+ }
14
+ //# sourceMappingURL=mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock.js","sourceRoot":"","sources":["../../../../src/rag/loaders/mock.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAcvC,MAAM,UAAU,UAAU,CAAC,UAA6B,EAAE;IACxD,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC;QAC3E,4DAA4D;QAC5D,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC5C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QAC9E,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { DocumentLoader } from '../types.js';
2
+ /**
3
+ * The slice of `unpdf` this loader uses — declared structurally, so a stub, a
4
+ * pinned fork, or a future version satisfies it without this package taking a
5
+ * hard type dependency on an optional peer.
6
+ */
7
+ export interface UnpdfBackend {
8
+ getDocumentProxy(data: Uint8Array): Promise<unknown>;
9
+ extractText(doc: unknown, options?: {
10
+ mergePages?: boolean;
11
+ }): Promise<{
12
+ totalPages: number;
13
+ text: string | string[];
14
+ }>;
15
+ }
16
+ export interface PdfLoaderOptions {
17
+ /**
18
+ * An ALREADY-IMPORTED `unpdf`. Supply this and the lazy load never happens —
19
+ * which is what makes the loader work in a BUNDLED app, where a bare
20
+ * specifier reaches the runtime unresolved:
21
+ *
22
+ * ```ts
23
+ * import * as unpdf from 'unpdf';
24
+ * pdfLoader({ backend: unpdf });
25
+ * ```
26
+ *
27
+ * The same mechanism the embedders and the store adapters use: the library
28
+ * states the surface it needs, the host owns the construction.
29
+ */
30
+ readonly backend?: UnpdfBackend;
31
+ }
32
+ /** Raised when a PDF is met and `unpdf` is not installed. */
33
+ export declare class MissingPdfSupportError extends Error {
34
+ readonly code: "ERR_MISSING_PDF_SUPPORT";
35
+ /** The file that could not be read. */
36
+ readonly uri: string;
37
+ constructor(uri: string);
38
+ }
39
+ export declare function pdfLoader(options?: PdfLoaderOptions): DocumentLoader;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * pdfLoader — PDF text extraction, page by page.
3
+ *
4
+ * Pattern: Adapter behind `DocumentLoader`, over a lazily-loaded optional peer.
5
+ * Role: rag/ layer. The ONE loader that needs a dependency, and the reason
6
+ * it is optional rather than bundled.
7
+ * Emits: N/A.
8
+ *
9
+ * ── Why `unpdf`, measured rather than assumed ───────────────────────────────
10
+ * PDF text extraction cannot be done without a real library. The candidates
11
+ * were installed and measured:
12
+ *
13
+ * | package | installed size | packages | deps | verdict |
14
+ * |---|---|---|---|---|
15
+ * | **`unpdf`** | **2.5 MB** | **1** | **none** | chosen |
16
+ * | `pdf-parse@2` | 86 MB | 3 | `@napi-rs/canvas` (**native**) | a native binary, to read text |
17
+ * | `pdf-parse@1` | 34 MB | 4 | debug, node-ensure | unmaintained since 2018 |
18
+ * | `pdfjs-dist@6` | 62 MB | 2 | none | 25× the size for the same engine |
19
+ *
20
+ * `unpdf` is a serverless build of the same pdf.js engine the heavyweight
21
+ * options wrap, with dual CJS/ESM exports and zero transitive dependencies.
22
+ * A real 2-page PDF extracts in ~95 ms.
23
+ *
24
+ * ── Pages are the point ─────────────────────────────────────────────────────
25
+ * It returns text PER PAGE, and that is why the page number in a citation is a
26
+ * fact rather than a guess. `<source id="handbook.pdf#7" page="2">` can be
27
+ * checked by opening page 2. A loader that flattened the document first could
28
+ * only ever say "somewhere in this PDF".
29
+ *
30
+ * ── A known warning on Node 22 ──────────────────────────────────────────────
31
+ * pdf.js v5 calls `Math.sumPrecise`, which arrives in V8 later than Node 22.
32
+ * The extraction is correct; the console shows `Warning: TypeError:
33
+ * Math.sumPrecise is not a function` per page. It is upstream and harmless,
34
+ * and it is documented here so it does not read as data loss.
35
+ */
36
+ import { lazyRequire } from '../../lib/lazyRequire.js';
37
+ /** Raised when a PDF is met and `unpdf` is not installed. */
38
+ export class MissingPdfSupportError extends Error {
39
+ code = 'ERR_MISSING_PDF_SUPPORT';
40
+ /** The file that could not be read. */
41
+ uri;
42
+ constructor(uri) {
43
+ super(`Reading '${uri}' requires the \`unpdf\` peer dependency.\n` +
44
+ ' Install: npm install unpdf\n' +
45
+ ' Or pass `backend` to pdfLoader() if your bundler resolves it statically.');
46
+ this.name = 'MissingPdfSupportError';
47
+ this.uri = uri;
48
+ }
49
+ }
50
+ export function pdfLoader(options = {}) {
51
+ let backend;
52
+ const getBackend = (uri) => {
53
+ if (options.backend)
54
+ return Promise.resolve(options.backend);
55
+ return (backend ??= (async () => {
56
+ // `unpdf` is ESM-only in its import condition, so a dynamic import is
57
+ // tried first and the CJS require is the fallback — one of the two works
58
+ // on every runtime this package supports.
59
+ try {
60
+ const spec = 'unpdf';
61
+ return (await import(spec));
62
+ }
63
+ catch {
64
+ try {
65
+ return lazyRequire('unpdf');
66
+ }
67
+ catch {
68
+ throw new MissingPdfSupportError(uri);
69
+ }
70
+ }
71
+ })());
72
+ };
73
+ return {
74
+ name: 'pdf',
75
+ extensions: ['.pdf'],
76
+ async load(input) {
77
+ const unpdf = await getBackend(input.uri);
78
+ const doc = await unpdf.getDocumentProxy(input.bytes);
79
+ const extracted = await unpdf.extractText(doc, { mergePages: false });
80
+ const pages = Array.isArray(extracted.text)
81
+ ? extracted.text.map((p) => String(p))
82
+ : [String(extracted.text)];
83
+ // Pages are joined with a blank line so `byParagraph` sees a boundary at
84
+ // every page break — and so the offsets a splitter computes index THIS
85
+ // string, which is the one that gets stored.
86
+ return { text: pages.join('\n\n'), pages };
87
+ },
88
+ };
89
+ }
90
+ //# sourceMappingURL=pdf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../../../src/rag/loaders/pdf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAiCvD,6DAA6D;AAC7D,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,IAAI,GAAG,yBAAkC,CAAC;IACnD,uCAAuC;IAC9B,GAAG,CAAS;IAErB,YAAY,GAAW;QACrB,KAAK,CACH,YAAY,GAAG,6CAA6C;YAC1D,iCAAiC;YACjC,4EAA4E,CAC/E,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;CACF;AAED,MAAM,UAAU,SAAS,CAAC,UAA4B,EAAE;IACtD,IAAI,OAA0C,CAAC;IAE/C,MAAM,UAAU,GAAG,CAAC,GAAW,EAAyB,EAAE;QACxD,IAAI,OAAO,CAAC,OAAO;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7D,OAAO,CAAC,OAAO,KAAK,CAAC,KAAK,IAA2B,EAAE;YACrD,sEAAsE;YACtE,yEAAyE;YACzE,0CAA0C;YAC1C,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,OAAO,CAAC;gBACrB,OAAO,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAA4B,CAAC;YACzD,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC;oBACH,OAAO,WAAW,CAAe,OAAO,CAAC,CAAC;gBAC5C,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;gBACxC,CAAC;YACH,CAAC;QACH,CAAC,CAAC,EAAE,CAAC,CAAC;IACR,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,KAAK;QACX,UAAU,EAAE,CAAC,MAAM,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC1C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACtD,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC;gBACzC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBACtC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7B,yEAAyE;YACzE,uEAAuE;YACvE,6CAA6C;YAC7C,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * textLoader — plain text, decoded and left alone.
3
+ *
4
+ * Pattern: Adapter behind `DocumentLoader`.
5
+ * Role: rag/ layer. The simplest possible loader, and the one that defines
6
+ * what "leaving positions alone" means for the others: it decodes
7
+ * UTF-8, normalises line endings, and does nothing else. No trimming,
8
+ * no collapsing of blank lines, no smart quotes — every one of those
9
+ * would shift every offset after it, and an offset that does not
10
+ * index the original is worse than no offset at all.
11
+ * Emits: N/A.
12
+ *
13
+ * Line-ending normalisation is the one exception, and it is a deliberate one:
14
+ * `\r\n` → `\n` happens BEFORE any offset is computed, so offsets index the
15
+ * normalised text, which is also the text that is stored and shown. The
16
+ * alternative — keeping `\r` — makes every paragraph boundary two characters
17
+ * on Windows and one everywhere else, so the same document chunks differently
18
+ * depending on who checked it out.
19
+ */
20
+ import type { DocumentLoader } from '../types.js';
21
+ /** Decode UTF-8 and normalise line endings. Shared by every text-shaped loader. */
22
+ export declare function decodeText(bytes: Uint8Array): string;
23
+ export declare function textLoader(): DocumentLoader;
@@ -0,0 +1,21 @@
1
+ /** Decode UTF-8 and normalise line endings. Shared by every text-shaped loader. */
2
+ export function decodeText(bytes) {
3
+ // `fatal: false` — a corpus is other people's files, and one bad byte in a
4
+ // 200-page document should degrade that character, not lose the document.
5
+ const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
6
+ // Strip a UTF-8 BOM: it is invisible, it is not content, and left in place it
7
+ // becomes the first character of the first chunk of the document.
8
+ const withoutBom = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
9
+ return withoutBom.replace(/\r\n/g, '\n');
10
+ }
11
+ export function textLoader() {
12
+ return {
13
+ name: 'text',
14
+ extensions: ['.txt', '.text', '.log', '.csv', '.tsv', '.json', '.yaml', '.yml'],
15
+ // eslint-disable-next-line @typescript-eslint/require-await
16
+ async load(input) {
17
+ return { text: decodeText(input.bytes) };
18
+ },
19
+ };
20
+ }
21
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../../../src/rag/loaders/text.ts"],"names":[],"mappings":"AAqBA,mFAAmF;AACnF,MAAM,UAAU,UAAU,CAAC,KAAiB;IAC1C,2EAA2E;IAC3E,0EAA0E;IAC1E,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACtE,8EAA8E;IAC9E,kEAAkE;IAClE,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxE,OAAO,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC;QAC/E,4DAA4D;QAC5D,KAAK,CAAC,IAAI,CAAC,KAAoB;YAC7B,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { Chunk, LoadedDocument, Splitter } from './types.js';
2
+ export interface SplitDocumentsOptions {
3
+ /**
4
+ * Verify each piece against its own offsets and throw on a mismatch.
5
+ * Default true. There is no good reason to turn it off except a splitter
6
+ * that deliberately rewrites text, in which case its citations cannot be
7
+ * checked against the source and you should know that.
8
+ */
9
+ readonly verifyOffsets?: boolean;
10
+ }
11
+ /**
12
+ * Cut documents into chunks.
13
+ *
14
+ * @throws when a splitter returns a piece whose offsets do not match its text.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const chunks = splitDocuments(documents, byHeading());
19
+ * ```
20
+ */
21
+ export declare function splitDocuments(documents: readonly LoadedDocument[], splitter: Splitter, options?: SplitDocumentsOptions): readonly Chunk[];
@@ -0,0 +1,65 @@
1
+ /**
2
+ * splitDocuments — documents become chunks that know where they came from.
3
+ *
4
+ * Pattern: application of the `Splitter` strategy, plus identity and hashing.
5
+ * Role: rag/ layer. Step two of the pipeline.
6
+ * Emits: N/A — `indexCorpus` wraps this in a stage and records what it did.
7
+ *
8
+ * The splitter decides WHERE to cut; this decides what a cut is CALLED and how
9
+ * a later run recognises it:
10
+ *
11
+ * - **id** is `'<docUri>#<index>'` — stable across runs for an unchanged
12
+ * document, human-readable, and the string the model is asked to cite.
13
+ * - **contentHash** is sha-256 of the chunk text. Two runs over an unchanged
14
+ * document produce identical hashes, which is what makes "skip this one"
15
+ * a fact rather than a guess about mtimes.
16
+ *
17
+ * The invariant every splitter must hold — `doc.text.slice(charStart, charEnd)
18
+ * === chunk.text` — is CHECKED here rather than trusted, because a custom
19
+ * splitter is a supported thing to write and a silently wrong offset produces
20
+ * citations that point at the wrong words.
21
+ */
22
+ import { sha256 } from './hash.js';
23
+ /**
24
+ * Cut documents into chunks.
25
+ *
26
+ * @throws when a splitter returns a piece whose offsets do not match its text.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const chunks = splitDocuments(documents, byHeading());
31
+ * ```
32
+ */
33
+ export function splitDocuments(documents, splitter, options = {}) {
34
+ const verify = options.verifyOffsets ?? true;
35
+ const chunks = [];
36
+ for (const doc of documents) {
37
+ const pieces = splitter.split(doc);
38
+ let index = 0;
39
+ for (const piece of pieces) {
40
+ if (verify && doc.text.slice(piece.charStart, piece.charEnd) !== piece.text) {
41
+ throw new Error(`splitter '${splitter.name}' produced a chunk of '${doc.uri}' whose offsets do not ` +
42
+ `match its text (chars ${piece.charStart}–${piece.charEnd}). A chunk that cannot be ` +
43
+ `located in its own document produces citations that point at the wrong words, so ` +
44
+ `this refuses rather than indexing it. Splitters must slice the document rather ` +
45
+ `than rebuild the text.`);
46
+ }
47
+ if (piece.text.length === 0)
48
+ continue;
49
+ chunks.push({
50
+ id: `${doc.uri}#${index}`,
51
+ docUri: doc.uri,
52
+ index,
53
+ text: piece.text,
54
+ charStart: piece.charStart,
55
+ charEnd: piece.charEnd,
56
+ ...(piece.page !== undefined && { page: piece.page }),
57
+ ...(piece.heading !== undefined && { heading: piece.heading }),
58
+ contentHash: sha256(piece.text),
59
+ });
60
+ index += 1;
61
+ }
62
+ }
63
+ return chunks;
64
+ }
65
+ //# sourceMappingURL=splitDocuments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splitDocuments.js","sourceRoot":"","sources":["../../../src/rag/splitDocuments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAanC;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,SAAoC,EACpC,QAAkB,EAClB,UAAiC,EAAE;IAEnC,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,IAAI,IAAI,CAAC;IAC7C,MAAM,MAAM,GAAY,EAAE,CAAC;IAE3B,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC;gBAC5E,MAAM,IAAI,KAAK,CACb,aAAa,QAAQ,CAAC,IAAI,0BAA0B,GAAG,CAAC,GAAG,yBAAyB;oBAClF,yBAAyB,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,OAAO,4BAA4B;oBACrF,mFAAmF;oBACnF,iFAAiF;oBACjF,wBAAwB,CAC3B,CAAC;YACJ,CAAC;YACD,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACtC,MAAM,CAAC,IAAI,CAAC;gBACV,EAAE,EAAE,GAAG,GAAG,CAAC,GAAG,IAAI,KAAK,EAAE;gBACzB,MAAM,EAAE,GAAG,CAAC,GAAG;gBACf,KAAK;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;gBACrD,GAAG,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;gBAC9D,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;aAChC,CAAC,CAAC;YACH,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * byHeading — cut where the author already said a section starts.
3
+ *
4
+ * Pattern: Strategy (one of `Splitter`).
5
+ * Role: rag/ layer. The default for Markdown, and the only splitter in this
6
+ * library that is **not a heuristic**.
7
+ * Emits: N/A.
8
+ *
9
+ * Every other strategy infers boundaries from typography. This one reads them:
10
+ * a Markdown `#` line is the document telling you where a section begins, and
11
+ * ignoring that in favour of a character count is throwing away the best
12
+ * information in the file.
13
+ *
14
+ * Each chunk carries its heading, which flows through to the citation the
15
+ * model sees — `<source id="refunds.md#3" heading="Refund timing">` — so a
16
+ * reader can find the passage by name rather than by counting characters.
17
+ *
18
+ * A section longer than `maxChars` is packed by paragraph within the section
19
+ * and, failing that, hard-cut; every piece keeps the heading it came from.
20
+ * Text before the first heading (a preamble, a title block) is its own
21
+ * heading-less section rather than being attached to a heading it precedes.
22
+ */
23
+ import type { Splitter } from '../types.js';
24
+ export interface ByHeadingOptions {
25
+ /** Target chunk size in characters. Default 1000. Sections longer than this are packed within. */
26
+ readonly maxChars?: number;
27
+ /** Backward overlap. Default 150. */
28
+ readonly overlapChars?: number;
29
+ /**
30
+ * Deepest heading level that starts a new section (1 = `#` only, 6 = all).
31
+ * Default 6 — every heading is a boundary. Lower it when a document uses
32
+ * `####` for emphasis rather than structure.
33
+ */
34
+ readonly maxLevel?: number;
35
+ }
36
+ export declare function byHeading(options?: ByHeadingOptions): Splitter;
@@ -0,0 +1,66 @@
1
+ import { DEFAULT_MAX_CHARS, DEFAULT_OVERLAP_CHARS } from './constants.js';
2
+ import { applyOverlap, foldRunts, hardCut, packSpans, paragraphSpans, toPieces, } from './shared.js';
3
+ /** ATX headings only (`# Title`). Setext (`Title\n=====`) is rare in docs corpora. */
4
+ const HEADING = /^(#{1,6})[ \t]+(.+?)[ \t]*$/gm;
5
+ export function byHeading(options = {}) {
6
+ const maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
7
+ const overlapChars = options.overlapChars ?? DEFAULT_OVERLAP_CHARS;
8
+ const maxLevel = options.maxLevel ?? 6;
9
+ return {
10
+ name: 'byHeading',
11
+ split(doc) {
12
+ const text = doc.text;
13
+ // Where each section starts, and what it is called.
14
+ const starts = [];
15
+ HEADING.lastIndex = 0;
16
+ let match;
17
+ while ((match = HEADING.exec(text)) !== null) {
18
+ if ((match[1] ?? '').length > maxLevel)
19
+ continue;
20
+ starts.push({ offset: match.index, heading: match[2] });
21
+ }
22
+ // A document with no headings at all is a paragraph document; say so by
23
+ // doing the paragraph thing rather than returning one giant chunk.
24
+ if (starts.length === 0) {
25
+ const packed = packSpans(paragraphSpans(text), maxChars);
26
+ const cut = packed.flatMap((s) => s.end - s.start > maxChars ? hardCut(s, maxChars, overlapChars) : [s]);
27
+ return toPieces(doc, applyOverlap(foldRunts(cut), overlapChars));
28
+ }
29
+ const sections = [];
30
+ // Preamble before the first heading — its own section, unnamed.
31
+ const firstStart = starts[0]?.offset ?? 0;
32
+ if (firstStart > 0)
33
+ sections.push({ start: 0, end: firstStart });
34
+ for (let i = 0; i < starts.length; i++) {
35
+ const start = starts[i]?.offset ?? 0;
36
+ const end = starts[i + 1]?.offset ?? text.length;
37
+ const heading = starts[i]?.heading;
38
+ sections.push({ start, end, ...(heading !== undefined && { heading }) });
39
+ }
40
+ // Sections that fit stay whole; the rest are packed by paragraph WITHIN
41
+ // the section, so a boundary never merges two headings' content.
42
+ const spans = [];
43
+ for (const section of sections) {
44
+ if (section.end - section.start <= maxChars) {
45
+ spans.push(section);
46
+ continue;
47
+ }
48
+ const inner = paragraphSpans(text.slice(section.start, section.end)).map((s) => ({
49
+ start: section.start + s.start,
50
+ end: section.start + s.end,
51
+ ...(section.heading !== undefined && { heading: section.heading }),
52
+ }));
53
+ for (const packedSpan of packSpans(inner, maxChars)) {
54
+ if (packedSpan.end - packedSpan.start > maxChars) {
55
+ spans.push(...hardCut(packedSpan, maxChars, overlapChars));
56
+ }
57
+ else {
58
+ spans.push(packedSpan);
59
+ }
60
+ }
61
+ }
62
+ return toPieces(doc, applyOverlap(foldRunts(spans), overlapChars));
63
+ },
64
+ };
65
+ }
66
+ //# sourceMappingURL=byHeading.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byHeading.js","sourceRoot":"","sources":["../../../../src/rag/splitters/byHeading.ts"],"names":[],"mappings":"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"}