@seanhogg/builderforce-memory 2026.7.14 → 2026.9.28

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 (225) hide show
  1. package/README.md +10 -0
  2. package/dist/bridges/AnthropicBridge.d.ts +9 -1
  3. package/dist/bridges/AnthropicBridge.d.ts.map +1 -1
  4. package/dist/bridges/AnthropicBridge.js +58 -2
  5. package/dist/bridges/AnthropicBridge.js.map +1 -1
  6. package/dist/bridges/CachingBridge.d.ts +9 -1
  7. package/dist/bridges/CachingBridge.d.ts.map +1 -1
  8. package/dist/bridges/CachingBridge.js +24 -1
  9. package/dist/bridges/CachingBridge.js.map +1 -1
  10. package/dist/bridges/OpenAIBridge.d.ts +4 -1
  11. package/dist/bridges/OpenAIBridge.d.ts.map +1 -1
  12. package/dist/bridges/OpenAIBridge.js +40 -2
  13. package/dist/bridges/OpenAIBridge.js.map +1 -1
  14. package/dist/bridges/SemanticCachingBridge.d.ts +8 -1
  15. package/dist/bridges/SemanticCachingBridge.d.ts.map +1 -1
  16. package/dist/bridges/SemanticCachingBridge.js +26 -1
  17. package/dist/bridges/SemanticCachingBridge.js.map +1 -1
  18. package/dist/bridges/TransformerBridge.d.ts +13 -0
  19. package/dist/bridges/TransformerBridge.d.ts.map +1 -1
  20. package/dist/bridges/VertexAIBridge.d.ts +102 -0
  21. package/dist/bridges/VertexAIBridge.d.ts.map +1 -0
  22. package/dist/bridges/VertexAIBridge.js +224 -0
  23. package/dist/bridges/VertexAIBridge.js.map +1 -0
  24. package/dist/bridges/index.d.ts +3 -1
  25. package/dist/bridges/index.d.ts.map +1 -1
  26. package/dist/bridges/index.js +1 -0
  27. package/dist/bridges/index.js.map +1 -1
  28. package/dist/eval/EvalHarness.d.ts +37 -0
  29. package/dist/eval/EvalHarness.d.ts.map +1 -0
  30. package/dist/eval/EvalHarness.js +178 -0
  31. package/dist/eval/EvalHarness.js.map +1 -0
  32. package/dist/eval/graders.d.ts +76 -0
  33. package/dist/eval/graders.d.ts.map +1 -0
  34. package/dist/eval/graders.js +168 -0
  35. package/dist/eval/graders.js.map +1 -0
  36. package/dist/eval/index.d.ts +26 -0
  37. package/dist/eval/index.d.ts.map +1 -0
  38. package/dist/eval/index.js +26 -0
  39. package/dist/eval/index.js.map +1 -0
  40. package/dist/eval/types.d.ts +97 -0
  41. package/dist/eval/types.d.ts.map +1 -0
  42. package/dist/eval/types.js +5 -0
  43. package/dist/eval/types.js.map +1 -0
  44. package/dist/evermind/contract.d.ts +40 -0
  45. package/dist/evermind/contract.d.ts.map +1 -0
  46. package/dist/evermind/contract.js +13 -0
  47. package/dist/evermind/contract.js.map +1 -0
  48. package/dist/evermind/index.d.ts +15 -0
  49. package/dist/evermind/index.d.ts.map +1 -0
  50. package/dist/evermind/index.js +4 -0
  51. package/dist/evermind/index.js.map +1 -0
  52. package/dist/evermind/memoryBlock.d.ts +7 -0
  53. package/dist/evermind/memoryBlock.d.ts.map +1 -0
  54. package/dist/evermind/memoryBlock.js +37 -0
  55. package/dist/evermind/memoryBlock.js.map +1 -0
  56. package/dist/evermind/recall.d.ts +48 -0
  57. package/dist/evermind/recall.d.ts.map +1 -0
  58. package/dist/evermind/recall.js +105 -0
  59. package/dist/evermind/recall.js.map +1 -0
  60. package/dist/index.d.ts +18 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +14 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/ingest/IngestionPipeline.d.ts +95 -0
  65. package/dist/ingest/IngestionPipeline.d.ts.map +1 -0
  66. package/dist/ingest/IngestionPipeline.js +295 -0
  67. package/dist/ingest/IngestionPipeline.js.map +1 -0
  68. package/dist/ingest/index.d.ts +10 -0
  69. package/dist/ingest/index.d.ts.map +1 -0
  70. package/dist/ingest/index.js +8 -0
  71. package/dist/ingest/index.js.map +1 -0
  72. package/dist/ingest/parsers.d.ts +53 -0
  73. package/dist/ingest/parsers.d.ts.map +1 -0
  74. package/dist/ingest/parsers.js +306 -0
  75. package/dist/ingest/parsers.js.map +1 -0
  76. package/dist/ingest/types.d.ts +114 -0
  77. package/dist/ingest/types.d.ts.map +1 -0
  78. package/dist/ingest/types.js +19 -0
  79. package/dist/ingest/types.js.map +1 -0
  80. package/dist/memory/MemoryStore.d.ts +33 -0
  81. package/dist/memory/MemoryStore.d.ts.map +1 -1
  82. package/dist/memory/MemoryStore.js +29 -3
  83. package/dist/memory/MemoryStore.js.map +1 -1
  84. package/dist/memory/index.d.ts +1 -1
  85. package/dist/memory/index.d.ts.map +1 -1
  86. package/dist/orchestration/AgentGraph.d.ts +104 -0
  87. package/dist/orchestration/AgentGraph.d.ts.map +1 -0
  88. package/dist/orchestration/AgentGraph.js +364 -0
  89. package/dist/orchestration/AgentGraph.js.map +1 -0
  90. package/dist/orchestration/checkpoint.d.ts +58 -0
  91. package/dist/orchestration/checkpoint.d.ts.map +1 -0
  92. package/dist/orchestration/checkpoint.js +92 -0
  93. package/dist/orchestration/checkpoint.js.map +1 -0
  94. package/dist/orchestration/index.d.ts +14 -0
  95. package/dist/orchestration/index.d.ts.map +1 -0
  96. package/dist/orchestration/index.js +10 -0
  97. package/dist/orchestration/index.js.map +1 -0
  98. package/dist/orchestration/patterns.d.ts +159 -0
  99. package/dist/orchestration/patterns.d.ts.map +1 -0
  100. package/dist/orchestration/patterns.js +371 -0
  101. package/dist/orchestration/patterns.js.map +1 -0
  102. package/dist/orchestration/types.d.ts +146 -0
  103. package/dist/orchestration/types.d.ts.map +1 -0
  104. package/dist/orchestration/types.js +20 -0
  105. package/dist/orchestration/types.js.map +1 -0
  106. package/dist/rag/EnterpriseRetriever.d.ts +114 -0
  107. package/dist/rag/EnterpriseRetriever.d.ts.map +1 -0
  108. package/dist/rag/EnterpriseRetriever.js +240 -0
  109. package/dist/rag/EnterpriseRetriever.js.map +1 -0
  110. package/dist/rag/index.d.ts +6 -0
  111. package/dist/rag/index.d.ts.map +1 -0
  112. package/dist/rag/index.js +5 -0
  113. package/dist/rag/index.js.map +1 -0
  114. package/dist/retrieval/bm25.d.ts +48 -1
  115. package/dist/retrieval/bm25.d.ts.map +1 -1
  116. package/dist/retrieval/bm25.js +59 -13
  117. package/dist/retrieval/bm25.js.map +1 -1
  118. package/dist/retrieval/index.d.ts +2 -2
  119. package/dist/retrieval/index.d.ts.map +1 -1
  120. package/dist/retrieval/index.js +1 -1
  121. package/dist/retrieval/index.js.map +1 -1
  122. package/dist/runtime/SSMRuntime.d.ts +1 -1
  123. package/dist/runtime/SSMRuntime.d.ts.map +1 -1
  124. package/dist/telemetry/InstrumentedBridge.d.ts +62 -0
  125. package/dist/telemetry/InstrumentedBridge.d.ts.map +1 -0
  126. package/dist/telemetry/InstrumentedBridge.js +120 -0
  127. package/dist/telemetry/InstrumentedBridge.js.map +1 -0
  128. package/dist/telemetry/MetricsRegistry.d.ts +88 -0
  129. package/dist/telemetry/MetricsRegistry.d.ts.map +1 -0
  130. package/dist/telemetry/MetricsRegistry.js +167 -0
  131. package/dist/telemetry/MetricsRegistry.js.map +1 -0
  132. package/dist/telemetry/Tracer.d.ts +137 -0
  133. package/dist/telemetry/Tracer.d.ts.map +1 -0
  134. package/dist/telemetry/Tracer.js +244 -0
  135. package/dist/telemetry/Tracer.js.map +1 -0
  136. package/dist/telemetry/exporters.d.ts +68 -0
  137. package/dist/telemetry/exporters.d.ts.map +1 -0
  138. package/dist/telemetry/exporters.js +145 -0
  139. package/dist/telemetry/exporters.js.map +1 -0
  140. package/dist/telemetry/index.d.ts +19 -0
  141. package/dist/telemetry/index.d.ts.map +1 -0
  142. package/dist/telemetry/index.js +12 -0
  143. package/dist/telemetry/index.js.map +1 -0
  144. package/dist/telemetry/pricing.d.ts +74 -0
  145. package/dist/telemetry/pricing.d.ts.map +1 -0
  146. package/dist/telemetry/pricing.js +98 -0
  147. package/dist/telemetry/pricing.js.map +1 -0
  148. package/dist/telemetry/traceparent.d.ts +50 -0
  149. package/dist/telemetry/traceparent.d.ts.map +1 -0
  150. package/dist/telemetry/traceparent.js +77 -0
  151. package/dist/telemetry/traceparent.js.map +1 -0
  152. package/dist/telemetry/types.d.ts +90 -0
  153. package/dist/telemetry/types.d.ts.map +1 -0
  154. package/dist/telemetry/types.js +17 -0
  155. package/dist/telemetry/types.js.map +1 -0
  156. package/dist/vectorstore/MemoryVectorStore.d.ts +72 -0
  157. package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -0
  158. package/dist/vectorstore/MemoryVectorStore.js +205 -0
  159. package/dist/vectorstore/MemoryVectorStore.js.map +1 -0
  160. package/dist/vectorstore/RestVectorStore.d.ts +79 -0
  161. package/dist/vectorstore/RestVectorStore.d.ts.map +1 -0
  162. package/dist/vectorstore/RestVectorStore.js +170 -0
  163. package/dist/vectorstore/RestVectorStore.js.map +1 -0
  164. package/dist/vectorstore/dialects.d.ts +101 -0
  165. package/dist/vectorstore/dialects.d.ts.map +1 -0
  166. package/dist/vectorstore/dialects.js +618 -0
  167. package/dist/vectorstore/dialects.js.map +1 -0
  168. package/dist/vectorstore/filter.d.ts +42 -0
  169. package/dist/vectorstore/filter.d.ts.map +1 -0
  170. package/dist/vectorstore/filter.js +132 -0
  171. package/dist/vectorstore/filter.js.map +1 -0
  172. package/dist/vectorstore/index.d.ts +15 -0
  173. package/dist/vectorstore/index.d.ts.map +1 -0
  174. package/dist/vectorstore/index.js +11 -0
  175. package/dist/vectorstore/index.js.map +1 -0
  176. package/dist/vectorstore/types.d.ts +151 -0
  177. package/dist/vectorstore/types.d.ts.map +1 -0
  178. package/dist/vectorstore/types.js +30 -0
  179. package/dist/vectorstore/types.js.map +1 -0
  180. package/package.json +39 -5
  181. package/src/bridges/AnthropicBridge.ts +68 -3
  182. package/src/bridges/CachingBridge.ts +28 -2
  183. package/src/bridges/OpenAIBridge.ts +48 -3
  184. package/src/bridges/SemanticCachingBridge.ts +29 -2
  185. package/src/bridges/TransformerBridge.ts +16 -0
  186. package/src/bridges/VertexAIBridge.ts +297 -0
  187. package/src/bridges/index.ts +3 -1
  188. package/src/eval/EvalHarness.ts +214 -0
  189. package/src/eval/graders.ts +218 -0
  190. package/src/eval/index.ts +26 -0
  191. package/src/eval/types.ts +107 -0
  192. package/src/evermind/contract.ts +39 -0
  193. package/src/evermind/index.ts +18 -0
  194. package/src/evermind/memoryBlock.ts +38 -0
  195. package/src/evermind/recall.ts +122 -0
  196. package/src/index.ts +232 -0
  197. package/src/ingest/IngestionPipeline.ts +378 -0
  198. package/src/ingest/index.ts +46 -0
  199. package/src/ingest/parsers.ts +318 -0
  200. package/src/ingest/types.ts +116 -0
  201. package/src/memory/MemoryStore.ts +55 -3
  202. package/src/memory/index.ts +1 -1
  203. package/src/orchestration/AgentGraph.ts +462 -0
  204. package/src/orchestration/checkpoint.ts +129 -0
  205. package/src/orchestration/index.ts +63 -0
  206. package/src/orchestration/patterns.ts +532 -0
  207. package/src/orchestration/types.ts +128 -0
  208. package/src/rag/EnterpriseRetriever.ts +348 -0
  209. package/src/rag/index.ts +18 -0
  210. package/src/retrieval/bm25.ts +71 -13
  211. package/src/retrieval/index.ts +2 -2
  212. package/src/telemetry/InstrumentedBridge.ts +152 -0
  213. package/src/telemetry/MetricsRegistry.ts +263 -0
  214. package/src/telemetry/Tracer.ts +310 -0
  215. package/src/telemetry/exporters.ts +187 -0
  216. package/src/telemetry/index.ts +56 -0
  217. package/src/telemetry/pricing.ts +129 -0
  218. package/src/telemetry/traceparent.ts +89 -0
  219. package/src/telemetry/types.ts +105 -0
  220. package/src/vectorstore/MemoryVectorStore.ts +250 -0
  221. package/src/vectorstore/RestVectorStore.ts +236 -0
  222. package/src/vectorstore/dialects.ts +702 -0
  223. package/src/vectorstore/filter.ts +148 -0
  224. package/src/vectorstore/index.ts +53 -0
  225. package/src/vectorstore/types.ts +149 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * ingest/parsers.ts — format handlers, as a registry.
3
+ *
4
+ * Each parser turns a source into sections and recovers whatever STRUCTURE the
5
+ * format carries, because structure that survives parsing becomes filterable
6
+ * metadata, and metadata is what makes retrieval precise:
7
+ *
8
+ * • Markdown keeps its heading path, so a chunk knows it lives under
9
+ * "Security › Data retention" and a citation can say so.
10
+ * • Rows keep every typed column, so `status = 'open'` is a database predicate
11
+ * rather than something the embedding has to imply.
12
+ * • JSON keeps its pointer, so a value can be traced back to its exact location.
13
+ *
14
+ * A parser that returned only text would throw all of that away and force the
15
+ * retriever to recover it statistically — the single most common reason a RAG
16
+ * pilot answers plausibly and cites nothing verifiable.
17
+ */
18
+
19
+ import type { MetadataRecord, MetadataValue } from '../vectorstore/types.js';
20
+ import type { DocumentParser, ParsedSection, SourceDocument, StructuredRow } from './types.js';
21
+
22
+ const textOf = (source: SourceDocument): string =>
23
+ source.content.kind === 'text' ? source.content.text : '';
24
+
25
+ const mediaTypeOf = (source: SourceDocument): string =>
26
+ source.content.kind === 'text' ? (source.content.mediaType ?? 'text/plain').toLowerCase() : '';
27
+
28
+ // ── structured rows ───────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Serialises a row as `field: value` lines.
32
+ *
33
+ * Field names are kept in the text on purpose: an embedding of `"status: open"`
34
+ * sits near a question about open items, whereas a bare `"open"` sits near
35
+ * nothing useful. The typed values are ALSO emitted as metadata, so the same fact
36
+ * is both semantically searchable and exactly filterable.
37
+ */
38
+ export function serializeRow(row: StructuredRow): string {
39
+ return Object.entries(row)
40
+ .filter(([, v]) => v !== null && v !== undefined && v !== '')
41
+ .map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : String(v)}`)
42
+ .join('\n');
43
+ }
44
+
45
+ export const rowsParser: DocumentParser = {
46
+ id: 'rows',
47
+ accepts: (source) => source.content.kind === 'rows',
48
+ parse: (source) => {
49
+ if (source.content.kind !== 'rows') return [];
50
+ const { rows, rowIdField } = source.content;
51
+ return rows.map((row, index) => {
52
+ const metadata: MetadataRecord = { ...row };
53
+ metadata['rowIndex'] = index;
54
+ if (rowIdField && row[rowIdField] !== undefined) {
55
+ metadata['rowId'] = String(row[rowIdField]);
56
+ }
57
+ return { text: serializeRow(row), metadata };
58
+ });
59
+ },
60
+ };
61
+
62
+ // ── CSV ───────────────────────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * RFC 4180 subset: quoted fields, doubled quotes, embedded newlines and commas.
66
+ * Enough for the exports enterprises actually hand over, without a dependency.
67
+ */
68
+ export function parseCsv(text: string, delimiter = ','): StructuredRow[] {
69
+ const rows: string[][] = [];
70
+ let field = '';
71
+ let record: string[] = [];
72
+ let quoted = false;
73
+
74
+ for (let i = 0; i < text.length; i++) {
75
+ const ch = text[i] as string;
76
+ if (quoted) {
77
+ if (ch === '"') {
78
+ if (text[i + 1] === '"') { field += '"'; i++; }
79
+ else quoted = false;
80
+ } else field += ch;
81
+ continue;
82
+ }
83
+ if (ch === '"') { quoted = true; continue; }
84
+ if (ch === delimiter) { record.push(field); field = ''; continue; }
85
+ if (ch === '\n' || ch === '\r') {
86
+ if (ch === '\r' && text[i + 1] === '\n') i++;
87
+ record.push(field);
88
+ rows.push(record);
89
+ record = [];
90
+ field = '';
91
+ continue;
92
+ }
93
+ field += ch;
94
+ }
95
+ if (field !== '' || record.length > 0) {
96
+ record.push(field);
97
+ rows.push(record);
98
+ }
99
+
100
+ const header = rows.shift();
101
+ if (!header) return [];
102
+
103
+ return rows
104
+ .filter((r) => r.some((cell) => cell.trim() !== ''))
105
+ .map((cells) => {
106
+ const row: StructuredRow = {};
107
+ header.forEach((name, i) => {
108
+ row[name.trim()] = coerce(cells[i] ?? '');
109
+ });
110
+ return row;
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Numbers and booleans are recovered from CSV text so range filters work.
116
+ * A value that merely *looks* numeric but has a leading zero (an account number,
117
+ * a zip code) stays a string — coercing it would silently destroy the identifier.
118
+ */
119
+ function coerce(raw: string): MetadataValue {
120
+ const value = raw.trim();
121
+ if (value === '') return '';
122
+ if (value === 'true') return true;
123
+ if (value === 'false') return false;
124
+ if (/^-?(0|[1-9]\d*)(\.\d+)?$/.test(value)) {
125
+ const n = Number(value);
126
+ if (Number.isFinite(n)) return n;
127
+ }
128
+ return value;
129
+ }
130
+
131
+ export const csvParser: DocumentParser = {
132
+ id: 'csv',
133
+ accepts: (source) => {
134
+ const type = mediaTypeOf(source);
135
+ return type.includes('csv') || type.includes('tab-separated');
136
+ },
137
+ parse: (source) => {
138
+ const delimiter = mediaTypeOf(source).includes('tab-separated') ? '\t' : ',';
139
+ const rows = parseCsv(textOf(source), delimiter);
140
+ return rowsParser.parse({ ...source, content: { kind: 'rows', rows } });
141
+ },
142
+ };
143
+
144
+ // ── JSON ──────────────────────────────────────────────────────────────────────
145
+
146
+ /**
147
+ * An array of objects is treated as rows (the common export shape); anything else
148
+ * is flattened to leaf paths so a value keeps its JSON pointer.
149
+ */
150
+ export const jsonParser: DocumentParser = {
151
+ id: 'json',
152
+ accepts: (source) => mediaTypeOf(source).includes('json'),
153
+ parse: (source) => {
154
+ let parsed: unknown;
155
+ try {
156
+ parsed = JSON.parse(textOf(source));
157
+ } catch {
158
+ // Malformed JSON is still text worth indexing; degrading beats dropping
159
+ // the document, and the parser name on the chunk records what happened.
160
+ return plainTextParser.parse(source);
161
+ }
162
+
163
+ if (Array.isArray(parsed) && parsed.every((v) => v && typeof v === 'object' && !Array.isArray(v))) {
164
+ return rowsParser.parse({
165
+ ...source,
166
+ content: { kind: 'rows', rows: parsed as StructuredRow[] },
167
+ });
168
+ }
169
+
170
+ const leaves = flatten(parsed, '');
171
+ if (leaves.length === 0) return [];
172
+ return [{
173
+ text: leaves.map(([path, value]) => `${path}: ${value}`).join('\n'),
174
+ metadata: { leafCount: leaves.length },
175
+ }];
176
+ },
177
+ };
178
+
179
+ function flatten(value: unknown, path: string, out: Array<[string, string]> = []): Array<[string, string]> {
180
+ if (value === null || typeof value !== 'object') {
181
+ out.push([path || '/', String(value)]);
182
+ return out;
183
+ }
184
+ if (Array.isArray(value)) {
185
+ value.forEach((v, i) => flatten(v, `${path}/${i}`, out));
186
+ return out;
187
+ }
188
+ for (const [key, v] of Object.entries(value as Record<string, unknown>)) {
189
+ flatten(v, `${path}/${key}`, out);
190
+ }
191
+ return out;
192
+ }
193
+
194
+ // ── Markdown ──────────────────────────────────────────────────────────────────
195
+
196
+ export const markdownParser: DocumentParser = {
197
+ id: 'markdown',
198
+ accepts: (source) => {
199
+ const type = mediaTypeOf(source);
200
+ return type.includes('markdown') || type.includes('/md');
201
+ },
202
+ parse: (source) => {
203
+ const lines = textOf(source).split('\n');
204
+ const sections: ParsedSection[] = [];
205
+ // Heading level -> text, so a chunk carries its full breadcrumb rather than
206
+ // only its nearest heading.
207
+ const path: string[] = [];
208
+ let buffer: string[] = [];
209
+
210
+ const flush = () => {
211
+ const text = buffer.join('\n').trim();
212
+ buffer = [];
213
+ if (!text) return;
214
+ const metadata: MetadataRecord = {};
215
+ if (path.length) {
216
+ metadata['headingPath'] = path.join(' › ');
217
+ metadata['heading'] = path[path.length - 1] as string;
218
+ metadata['headingDepth'] = path.length;
219
+ }
220
+ sections.push({ text, metadata });
221
+ };
222
+
223
+ let inFence = false;
224
+ for (const line of lines) {
225
+ if (/^\s*```/.test(line)) inFence = !inFence;
226
+ const heading = inFence ? null : /^(#{1,6})\s+(.*)$/.exec(line);
227
+ if (heading) {
228
+ flush();
229
+ const depth = (heading[1] as string).length;
230
+ path.length = Math.min(path.length, depth - 1);
231
+ path[depth - 1] = (heading[2] as string).trim();
232
+ path.length = depth;
233
+ continue;
234
+ }
235
+ buffer.push(line);
236
+ }
237
+ flush();
238
+ return sections;
239
+ },
240
+ };
241
+
242
+ // ── HTML ──────────────────────────────────────────────────────────────────────
243
+
244
+ export const htmlParser: DocumentParser = {
245
+ id: 'html',
246
+ accepts: (source) => mediaTypeOf(source).includes('html') || mediaTypeOf(source).includes('xml'),
247
+ parse: (source) => {
248
+ const raw = textOf(source);
249
+ const titleMatch = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(raw);
250
+
251
+ const text = raw
252
+ // script/style content is code, not prose — indexing it poisons recall.
253
+ .replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, ' ')
254
+ .replace(/<!--[\s\S]*?-->/g, ' ')
255
+ .replace(/<\/(p|div|section|article|li|tr|h[1-6])>/gi, '\n')
256
+ .replace(/<br\s*\/?>/gi, '\n')
257
+ .replace(/<[^>]+>/g, ' ')
258
+ .replace(/&nbsp;/g, ' ')
259
+ .replace(/&amp;/g, '&')
260
+ .replace(/&lt;/g, '<')
261
+ .replace(/&gt;/g, '>')
262
+ .replace(/&quot;/g, '"')
263
+ .replace(/&#39;/g, "'")
264
+ .replace(/[ \t]+/g, ' ')
265
+ .replace(/\n{3,}/g, '\n\n')
266
+ .trim();
267
+
268
+ if (!text) return [];
269
+ const metadata: MetadataRecord = {};
270
+ if (titleMatch?.[1]) metadata['htmlTitle'] = titleMatch[1].trim();
271
+ return [{ text, metadata }];
272
+ },
273
+ };
274
+
275
+ // ── plain text ────────────────────────────────────────────────────────────────
276
+
277
+ export const plainTextParser: DocumentParser = {
278
+ id: 'text',
279
+ accepts: (source) => source.content.kind === 'text',
280
+ parse: (source) => {
281
+ const text = textOf(source).trim();
282
+ return text ? [{ text }] : [];
283
+ },
284
+ };
285
+
286
+ // ── registry ──────────────────────────────────────────────────────────────────
287
+
288
+ /** Order matters: the first parser that accepts a source handles it. */
289
+ export const BUILTIN_PARSERS: readonly DocumentParser[] = [
290
+ rowsParser,
291
+ csvParser,
292
+ jsonParser,
293
+ markdownParser,
294
+ htmlParser,
295
+ plainTextParser,
296
+ ];
297
+
298
+ export class ParserRegistry {
299
+ private readonly _parsers: DocumentParser[];
300
+
301
+ constructor(parsers: readonly DocumentParser[] = BUILTIN_PARSERS) {
302
+ this._parsers = [...parsers];
303
+ }
304
+
305
+ /** Registers a parser at the FRONT, so it can override a builtin. */
306
+ register(parser: DocumentParser): this {
307
+ this._parsers.unshift(parser);
308
+ return this;
309
+ }
310
+
311
+ resolve(source: SourceDocument): DocumentParser | undefined {
312
+ return this._parsers.find((p) => p.accepts(source));
313
+ }
314
+
315
+ list(): string[] {
316
+ return this._parsers.map((p) => p.id);
317
+ }
318
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * ingest/types.ts — the ingestion vocabulary.
3
+ *
4
+ * The pipeline handles STRUCTURED and UNSTRUCTURED sources through one path, which
5
+ * is the point. A support ticket export (rows, typed columns, foreign keys) and a
6
+ * policy PDF's text both end up as chunks with typed metadata; the difference is
7
+ * which parser ran, not which pipeline. That is what makes a query like
8
+ * "summarise open P1 tickets about billing" answerable — `priority` and `status`
9
+ * survive as *filterable metadata* instead of being flattened into prose the
10
+ * embedding has to recover.
11
+ */
12
+
13
+ import type { MetadataRecord, MetadataValue } from '../vectorstore/types.js';
14
+
15
+ /** One row of a structured source (a DB result set, a CSV, a spreadsheet). */
16
+ export type StructuredRow = Record<string, MetadataValue>;
17
+
18
+ export type SourceContent =
19
+ | { kind: 'text'; text: string; mediaType?: string }
20
+ | { kind: 'rows'; rows: StructuredRow[]; rowIdField?: string };
21
+
22
+ export interface SourceDocument {
23
+ /** Stable id in the ORIGIN system. Re-ingesting the same id replaces its chunks. */
24
+ id: string;
25
+ content: SourceContent;
26
+ /** Owning tenant. Written to every chunk and enforced on every read. */
27
+ tenantId: string;
28
+ /**
29
+ * Principal ids permitted to retrieve this document. Defaults to `['*']`
30
+ * (tenant-public). Written to every chunk so ACL evaluation happens inside the
31
+ * database query, not after the fact.
32
+ */
33
+ acl?: string[];
34
+ /** Classification level; readers declare a ceiling via `AccessScope`. */
35
+ sensitivity?: number;
36
+ /** Arbitrary filterable metadata copied onto every chunk. */
37
+ metadata?: MetadataRecord;
38
+ /** Human-facing title, used in citations. */
39
+ title?: string;
40
+ /** Origin URI, used in citations. */
41
+ uri?: string;
42
+ }
43
+
44
+ /** A parser's output: a span of text plus whatever structure it recovered. */
45
+ export interface ParsedSection {
46
+ text: string;
47
+ /** Section-scoped metadata — heading path, row id, JSON pointer, … */
48
+ metadata?: MetadataRecord;
49
+ }
50
+
51
+ /**
52
+ * Parsers are registry entries, so a new format (PDF text layer, DOCX, a bespoke
53
+ * log) is registered rather than branched into the pipeline.
54
+ */
55
+ export interface DocumentParser {
56
+ readonly id: string;
57
+ /** True when this parser handles the source. First match in registry order wins. */
58
+ accepts(source: SourceDocument): boolean;
59
+ parse(source: SourceDocument): ParsedSection[];
60
+ }
61
+
62
+ /** Identity of one stored chunk, for incremental sync. */
63
+ export interface ChunkFingerprint {
64
+ id: string;
65
+ /** Content hash — an unchanged hash means the chunk need not be re-embedded. */
66
+ hash: string;
67
+ }
68
+
69
+ /**
70
+ * Records which chunks each source produced last time.
71
+ *
72
+ * Supplying one enables `diff` sync: only changed chunks are re-embedded (the
73
+ * expensive step) and only vanished chunks are deleted. Without it the pipeline
74
+ * falls back to `replace`, which is correct but re-embeds everything.
75
+ */
76
+ export interface IngestManifest {
77
+ get(sourceId: string): Promise<ChunkFingerprint[] | undefined>;
78
+ set(sourceId: string, fingerprints: ChunkFingerprint[]): Promise<void>;
79
+ delete(sourceId: string): Promise<void>;
80
+ }
81
+
82
+ /** Batch embedder. Batched because per-text calls are the classic N+1 of RAG. */
83
+ export type BatchEmbedder = (texts: string[]) => Promise<Float32Array[]>;
84
+
85
+ export type SyncStrategy =
86
+ /** Re-embed only changed chunks, delete only vanished ones. Needs a manifest. */
87
+ | 'diff'
88
+ /** Delete the source's existing chunks, then write the new set. */
89
+ | 'replace'
90
+ /** Write without deleting anything — for append-only corpora. */
91
+ | 'append';
92
+
93
+ export interface IngestionReport {
94
+ documents: number;
95
+ sections: number;
96
+ chunks: number;
97
+ /** Chunks sent to the embedder — the cost driver. */
98
+ embedded: number;
99
+ upserted: number;
100
+ /** Chunks whose hash was unchanged and were skipped entirely. */
101
+ skippedUnchanged: number;
102
+ /** Chunks deleted because they no longer exist in the source. */
103
+ deletedStale: number;
104
+ failures: Array<{ sourceId: string; error: string }>;
105
+ durationMs: number;
106
+ /** Trace id covering the whole run, when a tracer is attached. */
107
+ traceId?: string;
108
+ }
109
+
110
+ /** Metadata keys the pipeline reserves on every chunk it writes. */
111
+ export const SOURCE_ID_FIELD = 'sourceId';
112
+ export const CHUNK_INDEX_FIELD = 'chunkIndex';
113
+ export const CONTENT_HASH_FIELD = 'contentHash';
114
+ export const TITLE_FIELD = 'title';
115
+ export const URI_FIELD = 'uri';
116
+ export const PARSER_FIELD = 'parser';
@@ -36,6 +36,26 @@ export interface MemoryEntry {
36
36
  importance?: number;
37
37
  }
38
38
 
39
+ /**
40
+ * Which ranker produced a recall ordering. Matches the vocabulary the gateway's
41
+ * Evermind recall/validate surfaces already report, so a host can render one chip
42
+ * ("Semantic recall" / "Lexical recall (fallback)") regardless of which layer
43
+ * answered.
44
+ */
45
+ export type RecallMethod = 'embedding' | 'lexical';
46
+
47
+ /** A recalled entry with its relevance score (higher = closer). */
48
+ export interface ScoredMemory {
49
+ entry: MemoryEntry;
50
+ score: number;
51
+ }
52
+
53
+ /** A ranked recall: the hits plus the ranker that ordered them. */
54
+ export interface RankedRecall {
55
+ hits: ScoredMemory[];
56
+ method: RecallMethod;
57
+ }
58
+
39
59
  export interface RememberOptions {
40
60
  /** Override the store-level defaultTtlMs for this entry. */
41
61
  ttlMs? : number;
@@ -390,8 +410,32 @@ export class MemoryStore {
390
410
  runtime?: SSMRuntimeRef,
391
411
  opts?: HybridRetrieveOptions,
392
412
  ): Promise<MemoryEntry[]> {
413
+ return (await this.recallRanked(query, topK, runtime, opts)).hits.map(h => h.entry);
414
+ }
415
+
416
+ /**
417
+ * The hybrid retrieval path with its evidence attached: each hit's fused score,
418
+ * plus WHICH ranker produced the ordering.
419
+ *
420
+ * {@link recallHybrid} is the convenience wrapper that throws both away, so
421
+ * there is exactly one implementation of the blend — and it is the shared
422
+ * `hybridRetrieve` → `reciprocalRankFusion` primitive, never a second hand-
423
+ * rolled mix of cosine and word overlap.
424
+ *
425
+ * `method` is honest rather than aspirational: `'embedding'` means the query
426
+ * really did embed and the dense ranking really did participate in the fusion;
427
+ * `'lexical'` means no model was available (or embedding failed) and the
428
+ * ordering is BM25 alone. Callers surface it so a user can tell semantic
429
+ * recall from the fallback instead of guessing.
430
+ */
431
+ async recallRanked(
432
+ query: string,
433
+ topK: number,
434
+ runtime?: SSMRuntimeRef,
435
+ opts?: HybridRetrieveOptions,
436
+ ): Promise<RankedRecall> {
393
437
  const all = await this.recallAll();
394
- if (all.length === 0) return [];
438
+ if (all.length === 0) return { hits: [], method: 'lexical' };
395
439
 
396
440
  // Embed candidates + query where a runtime is available; null vectors are
397
441
  // fine — hybridRetrieve degrades that candidate to BM25-only.
@@ -400,13 +444,21 @@ export class MemoryStore {
400
444
 
401
445
  const candidates: RetrievalCandidate[] = [];
402
446
  for (const entry of all) {
403
- const vector = canEmbed ? (await this._embedWithCache(runtime, entry.content)) ?? undefined : undefined;
447
+ const vector = queryVec ? (await this._embedWithCache(runtime, entry.content)) ?? undefined : undefined;
404
448
  candidates.push({ id: entry.key, text: entry.content, vector });
405
449
  }
406
450
 
451
+ // The dense ranking only exists when the query embedded AND at least one
452
+ // candidate did — anything less is a lexical ordering wearing a label.
453
+ const dense = !!queryVec && candidates.some(c => !!c.vector);
407
454
  const hits = hybridRetrieve({ text: query, vector: queryVec }, candidates, { topK, annThreshold: this._annThreshold, ...opts });
408
455
  const byKey = new Map(all.map(e => [e.key, e]));
409
- return hits.map(h => byKey.get(h.id)).filter((e): e is MemoryEntry => !!e);
456
+ return {
457
+ hits: hits
458
+ .map(h => { const entry = byKey.get(h.id); return entry ? { entry, score: h.score } : null; })
459
+ .filter((h): h is ScoredMemory => !!h),
460
+ method: dense ? 'embedding' : 'lexical',
461
+ };
410
462
  }
411
463
 
412
464
  /**
@@ -1,2 +1,2 @@
1
1
  export { MemoryStore } from './MemoryStore.js';
2
- export type { MemoryEntry, MemoryStoreOptions, RememberOptions, FactType } from './MemoryStore.js';
2
+ export type { MemoryEntry, MemoryStoreOptions, RememberOptions, FactType, RecallMethod, ScoredMemory, RankedRecall } from './MemoryStore.js';