@postalsys/email-content 0.0.1

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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/analyze.d.ts +91 -0
  5. package/dist/analyze.d.ts.map +1 -0
  6. package/dist/analyze.js +274 -0
  7. package/dist/analyze.js.map +1 -0
  8. package/dist/annotate/client.d.ts +64 -0
  9. package/dist/annotate/client.d.ts.map +1 -0
  10. package/dist/annotate/client.js +139 -0
  11. package/dist/annotate/client.js.map +1 -0
  12. package/dist/annotate/llm.d.ts +46 -0
  13. package/dist/annotate/llm.d.ts.map +1 -0
  14. package/dist/annotate/llm.js +107 -0
  15. package/dist/annotate/llm.js.map +1 -0
  16. package/dist/annotate/prompt.d.ts +38 -0
  17. package/dist/annotate/prompt.d.ts.map +1 -0
  18. package/dist/annotate/prompt.js +165 -0
  19. package/dist/annotate/prompt.js.map +1 -0
  20. package/dist/annotate/store.d.ts +43 -0
  21. package/dist/annotate/store.d.ts.map +1 -0
  22. package/dist/annotate/store.js +46 -0
  23. package/dist/annotate/store.js.map +1 -0
  24. package/dist/canonical/html.d.ts +33 -0
  25. package/dist/canonical/html.d.ts.map +1 -0
  26. package/dist/canonical/html.js +156 -0
  27. package/dist/canonical/html.js.map +1 -0
  28. package/dist/canonical/text.d.ts +75 -0
  29. package/dist/canonical/text.d.ts.map +1 -0
  30. package/dist/canonical/text.js +217 -0
  31. package/dist/canonical/text.js.map +1 -0
  32. package/dist/classify/confidence.d.ts +38 -0
  33. package/dist/classify/confidence.d.ts.map +1 -0
  34. package/dist/classify/confidence.js +59 -0
  35. package/dist/classify/confidence.js.map +1 -0
  36. package/dist/classify/reconcile.d.ts +52 -0
  37. package/dist/classify/reconcile.d.ts.map +1 -0
  38. package/dist/classify/reconcile.js +100 -0
  39. package/dist/classify/reconcile.js.map +1 -0
  40. package/dist/classify/rules.d.ts +51 -0
  41. package/dist/classify/rules.d.ts.map +1 -0
  42. package/dist/classify/rules.js +648 -0
  43. package/dist/classify/rules.js.map +1 -0
  44. package/dist/cli/annotate.d.ts +23 -0
  45. package/dist/cli/annotate.d.ts.map +1 -0
  46. package/dist/cli/annotate.js +328 -0
  47. package/dist/cli/annotate.js.map +1 -0
  48. package/dist/cli/args.d.ts +17 -0
  49. package/dist/cli/args.d.ts.map +1 -0
  50. package/dist/cli/args.js +72 -0
  51. package/dist/cli/args.js.map +1 -0
  52. package/dist/cli/benchmark.d.ts +13 -0
  53. package/dist/cli/benchmark.d.ts.map +1 -0
  54. package/dist/cli/benchmark.js +172 -0
  55. package/dist/cli/benchmark.js.map +1 -0
  56. package/dist/cli/corpus.d.ts +9 -0
  57. package/dist/cli/corpus.d.ts.map +1 -0
  58. package/dist/cli/corpus.js +152 -0
  59. package/dist/cli/corpus.js.map +1 -0
  60. package/dist/cli/disagreements.d.ts +14 -0
  61. package/dist/cli/disagreements.d.ts.map +1 -0
  62. package/dist/cli/disagreements.js +133 -0
  63. package/dist/cli/disagreements.js.map +1 -0
  64. package/dist/cli/evaluate.d.ts +15 -0
  65. package/dist/cli/evaluate.d.ts.map +1 -0
  66. package/dist/cli/evaluate.js +215 -0
  67. package/dist/cli/evaluate.js.map +1 -0
  68. package/dist/cli/extract.d.ts +10 -0
  69. package/dist/cli/extract.d.ts.map +1 -0
  70. package/dist/cli/extract.js +139 -0
  71. package/dist/cli/extract.js.map +1 -0
  72. package/dist/cli/main.d.ts +9 -0
  73. package/dist/cli/main.d.ts.map +1 -0
  74. package/dist/cli/main.js +147 -0
  75. package/dist/cli/main.js.map +1 -0
  76. package/dist/corpus/analyze.d.ts +66 -0
  77. package/dist/corpus/analyze.d.ts.map +1 -0
  78. package/dist/corpus/analyze.js +214 -0
  79. package/dist/corpus/analyze.js.map +1 -0
  80. package/dist/corpus/migrations/index.d.ts +14 -0
  81. package/dist/corpus/migrations/index.d.ts.map +1 -0
  82. package/dist/corpus/migrations/index.js +158 -0
  83. package/dist/corpus/migrations/index.js.map +1 -0
  84. package/dist/corpus/sample.d.ts +66 -0
  85. package/dist/corpus/sample.d.ts.map +1 -0
  86. package/dist/corpus/sample.js +98 -0
  87. package/dist/corpus/sample.js.map +1 -0
  88. package/dist/corpus/scanner.d.ts +79 -0
  89. package/dist/corpus/scanner.d.ts.map +1 -0
  90. package/dist/corpus/scanner.js +256 -0
  91. package/dist/corpus/scanner.js.map +1 -0
  92. package/dist/corpus/stats.d.ts +76 -0
  93. package/dist/corpus/stats.d.ts.map +1 -0
  94. package/dist/corpus/stats.js +240 -0
  95. package/dist/corpus/stats.js.map +1 -0
  96. package/dist/corpus/store.d.ts +67 -0
  97. package/dist/corpus/store.d.ts.map +1 -0
  98. package/dist/corpus/store.js +196 -0
  99. package/dist/corpus/store.js.map +1 -0
  100. package/dist/corpus/thread-index.d.ts +31 -0
  101. package/dist/corpus/thread-index.d.ts.map +1 -0
  102. package/dist/corpus/thread-index.js +182 -0
  103. package/dist/corpus/thread-index.js.map +1 -0
  104. package/dist/corpus/worker.d.ts +21 -0
  105. package/dist/corpus/worker.d.ts.map +1 -0
  106. package/dist/corpus/worker.js +74 -0
  107. package/dist/corpus/worker.js.map +1 -0
  108. package/dist/detect/client.d.ts +28 -0
  109. package/dist/detect/client.d.ts.map +1 -0
  110. package/dist/detect/client.js +121 -0
  111. package/dist/detect/client.js.map +1 -0
  112. package/dist/detect/disclaimer.d.ts +12 -0
  113. package/dist/detect/disclaimer.d.ts.map +1 -0
  114. package/dist/detect/disclaimer.js +28 -0
  115. package/dist/detect/disclaimer.js.map +1 -0
  116. package/dist/detect/forward-header.d.ts +10 -0
  117. package/dist/detect/forward-header.d.ts.map +1 -0
  118. package/dist/detect/forward-header.js +35 -0
  119. package/dist/detect/forward-header.js.map +1 -0
  120. package/dist/detect/patterns/disclaimers.json +113 -0
  121. package/dist/detect/patterns/forward-headers.json +40 -0
  122. package/dist/detect/patterns/html-clients.json +157 -0
  123. package/dist/detect/patterns/quoted-headers.json +41 -0
  124. package/dist/detect/patterns/reply-headers.json +139 -0
  125. package/dist/detect/patterns/signatures.json +58 -0
  126. package/dist/detect/patterns.d.ts +43 -0
  127. package/dist/detect/patterns.d.ts.map +1 -0
  128. package/dist/detect/patterns.js +125 -0
  129. package/dist/detect/patterns.js.map +1 -0
  130. package/dist/detect/reply-header.d.ts +44 -0
  131. package/dist/detect/reply-header.d.ts.map +1 -0
  132. package/dist/detect/reply-header.js +134 -0
  133. package/dist/detect/reply-header.js.map +1 -0
  134. package/dist/detect/signature.d.ts +28 -0
  135. package/dist/detect/signature.d.ts.map +1 -0
  136. package/dist/detect/signature.js +77 -0
  137. package/dist/detect/signature.js.map +1 -0
  138. package/dist/detect/traffic.d.ts +26 -0
  139. package/dist/detect/traffic.d.ts.map +1 -0
  140. package/dist/detect/traffic.js +98 -0
  141. package/dist/detect/traffic.js.map +1 -0
  142. package/dist/eval/metrics.d.ts +87 -0
  143. package/dist/eval/metrics.d.ts.map +1 -0
  144. package/dist/eval/metrics.js +185 -0
  145. package/dist/eval/metrics.js.map +1 -0
  146. package/dist/index.d.ts +54 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +163 -0
  149. package/dist/index.js.map +1 -0
  150. package/dist/match/history.d.ts +62 -0
  151. package/dist/match/history.d.ts.map +1 -0
  152. package/dist/match/history.js +128 -0
  153. package/dist/match/history.js.map +1 -0
  154. package/dist/match/normalize.d.ts +33 -0
  155. package/dist/match/normalize.d.ts.map +1 -0
  156. package/dist/match/normalize.js +63 -0
  157. package/dist/match/normalize.js.map +1 -0
  158. package/dist/match/shingle.d.ts +44 -0
  159. package/dist/match/shingle.d.ts.map +1 -0
  160. package/dist/match/shingle.js +78 -0
  161. package/dist/match/shingle.js.map +1 -0
  162. package/dist/mime/charset.d.ts +33 -0
  163. package/dist/mime/charset.d.ts.map +1 -0
  164. package/dist/mime/charset.js +118 -0
  165. package/dist/mime/charset.js.map +1 -0
  166. package/dist/mime/headers.d.ts +32 -0
  167. package/dist/mime/headers.d.ts.map +1 -0
  168. package/dist/mime/headers.js +127 -0
  169. package/dist/mime/headers.js.map +1 -0
  170. package/dist/mime/model.d.ts +111 -0
  171. package/dist/mime/model.d.ts.map +1 -0
  172. package/dist/mime/model.js +17 -0
  173. package/dist/mime/model.js.map +1 -0
  174. package/dist/mime/parse.d.ts +27 -0
  175. package/dist/mime/parse.d.ts.map +1 -0
  176. package/dist/mime/parse.js +343 -0
  177. package/dist/mime/parse.js.map +1 -0
  178. package/dist/segment/features.d.ts +29 -0
  179. package/dist/segment/features.d.ts.map +1 -0
  180. package/dist/segment/features.js +102 -0
  181. package/dist/segment/features.js.map +1 -0
  182. package/dist/segment/html-blocks.d.ts +28 -0
  183. package/dist/segment/html-blocks.d.ts.map +1 -0
  184. package/dist/segment/html-blocks.js +596 -0
  185. package/dist/segment/html-blocks.js.map +1 -0
  186. package/dist/segment/text-blocks.d.ts +30 -0
  187. package/dist/segment/text-blocks.d.ts.map +1 -0
  188. package/dist/segment/text-blocks.js +204 -0
  189. package/dist/segment/text-blocks.js.map +1 -0
  190. package/dist/segment/types.d.ts +88 -0
  191. package/dist/segment/types.d.ts.map +1 -0
  192. package/dist/segment/types.js +11 -0
  193. package/dist/segment/types.js.map +1 -0
  194. package/dist/types.d.ts +85 -0
  195. package/dist/types.d.ts.map +1 -0
  196. package/dist/types.js +17 -0
  197. package/dist/types.js.map +1 -0
  198. package/dist/util/hash.d.ts +49 -0
  199. package/dist/util/hash.d.ts.map +1 -0
  200. package/dist/util/hash.js +0 -0
  201. package/dist/util/hash.js.map +1 -0
  202. package/dist/util/lang.d.ts +25 -0
  203. package/dist/util/lang.d.ts.map +1 -0
  204. package/dist/util/lang.js +135 -0
  205. package/dist/util/lang.js.map +1 -0
  206. package/dist/util/worker-pool.d.ts +51 -0
  207. package/dist/util/worker-pool.d.ts.map +1 -0
  208. package/dist/util/worker-pool.js +155 -0
  209. package/dist/util/worker-pool.js.map +1 -0
  210. package/package.json +74 -0
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ /**
3
+ * Content first API.
4
+ *
5
+ * `extract()` takes a raw RFC 5322 message and does its own MIME parsing. Most callers do not have
6
+ * one: they have already parsed the message, with mailparser, postal-mime, EmailEngine or an IMAP
7
+ * client, and hold `{ text, html, subject }`. Re-serialising that back into a message just to parse
8
+ * it again would be wasteful and lossy.
9
+ *
10
+ * `analyze()` is the same pipeline entered one stage later. Stage 1 is skipped, Stages 2 to 8 are
11
+ * identical, and the result has the same shape. Everything the extractor decides is derived from
12
+ * the bodies themselves, so nothing is lost by not having the envelope.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.senderTokensFrom = senderTokensFrom;
16
+ exports.resolveHistory = resolveHistory;
17
+ exports.analyze = analyze;
18
+ const html_blocks_1 = require("./segment/html-blocks");
19
+ const text_blocks_1 = require("./segment/text-blocks");
20
+ const rules_1 = require("./classify/rules");
21
+ const reconcile_1 = require("./classify/reconcile");
22
+ const confidence_1 = require("./classify/confidence");
23
+ const history_1 = require("./match/history");
24
+ const types_1 = require("./types");
25
+ const REPLY_SUBJECT = /^\s*(re|aw|sv|vs|vast|antw|res|odp|ответ)\s*(\[\d+\])?\s*:/i;
26
+ function contentChars(blocks) {
27
+ return blocks.reduce((sum, block) => sum + block.content.length, 0);
28
+ }
29
+ function hasAnchor(blocks) {
30
+ return blocks.some(block => block.evidence.some(evidence => evidence.kind === 'reply-anchor' || evidence.kind === 'forward-anchor' || (evidence.kind === 'quote-container' && evidence.strength >= 0.85)));
31
+ }
32
+ /**
33
+ * Name tokens from a From header, for signature detection.
34
+ *
35
+ * Takes the display name words and the address local part, keeping tokens long enough to be
36
+ * distinctive. Short tokens are dropped because a two letter name fragment matches everywhere.
37
+ */
38
+ function senderTokensFrom(from) {
39
+ if (!from) {
40
+ return [];
41
+ }
42
+ const display = from.replace(/<[^>]*>/g, ' ').replace(/["']/g, ' ');
43
+ const local = (from.match(/([\w.+-]{1,64})@/) || [])[1] ?? '';
44
+ const tokens = new Set();
45
+ for (const token of `${display} ${local.replace(/[._+-]+/g, ' ')}`.toLowerCase().split(/[^\p{L}\p{N}]+/u)) {
46
+ if (token.length >= 3) {
47
+ tokens.add(token);
48
+ }
49
+ }
50
+ return [...tokens];
51
+ }
52
+ function buildRepresentation(blocks, representation, config, history, senderTokens) {
53
+ const matches = history.length ? (0, history_1.matchBlocks)(blocks, history) : [];
54
+ return {
55
+ representation,
56
+ decisions: (0, rules_1.classifyBlocks)(blocks, { config, historyMatchedIndices: (0, history_1.matchedIndices)(matches), senderTokens }),
57
+ contentChars: contentChars(blocks),
58
+ hasAnchor: hasAnchor(blocks),
59
+ matches
60
+ };
61
+ }
62
+ /** Merge adjacent decisions carrying the same label, so the output is regions rather than lines. */
63
+ function toSegments(decisions, config) {
64
+ const segments = [];
65
+ for (const decision of decisions) {
66
+ const block = decision.block;
67
+ const previous = segments[segments.length - 1];
68
+ const visibility = (0, confidence_1.visibilityFor)(decision.label, decision.confidence, config);
69
+ if (previous && previous.type === decision.label && previous.visibility === visibility && previous.source.mimePart === block.mimePart) {
70
+ previous.text = previous.text ? `${previous.text}\n${block.content}` : block.content;
71
+ if (block.html) {
72
+ previous.html = (previous.html ?? '') + block.html;
73
+ }
74
+ previous.source.end = block.end;
75
+ previous.confidence = Math.min(previous.confidence, decision.confidence);
76
+ for (const reason of decision.reasons) {
77
+ if (!previous.reasons.includes(reason)) {
78
+ previous.reasons.push(reason);
79
+ }
80
+ }
81
+ continue;
82
+ }
83
+ if (block.features.isAllBlank && !previous) {
84
+ continue;
85
+ }
86
+ segments.push({
87
+ type: decision.label,
88
+ text: block.content,
89
+ ...(block.html ? { html: block.html } : {}),
90
+ confidence: decision.confidence,
91
+ visibility,
92
+ reasons: [...decision.reasons],
93
+ source: {
94
+ mimePart: block.mimePart,
95
+ representation: block.representation,
96
+ blockIndex: block.index,
97
+ start: block.start,
98
+ end: block.end,
99
+ ...(block.domPath ? { domPath: block.domPath } : {})
100
+ }
101
+ });
102
+ }
103
+ return segments.filter(segment => segment.text.trim().length > 0);
104
+ }
105
+ /** Turn whatever the caller supplied as history into searchable documents. */
106
+ async function resolveHistory(options) {
107
+ if (options.historyDocuments?.length) {
108
+ return [...options.historyDocuments];
109
+ }
110
+ const documents = [];
111
+ for (let i = 0; i < (options.history?.length ?? 0); i++) {
112
+ const item = options.history[i];
113
+ try {
114
+ if (typeof item === 'string' || Buffer.isBuffer(item)) {
115
+ documents.push(await (0, history_1.buildHistoryDocument)(item, `history-${i}`, i === 0));
116
+ }
117
+ else if (item) {
118
+ documents.push((0, history_1.buildHistoryDocumentFromContent)(item.text ?? '', item.html ?? '', `history-${i}`, i === 0));
119
+ }
120
+ }
121
+ catch {
122
+ // An unusable ancestor simply contributes no evidence.
123
+ }
124
+ }
125
+ return documents;
126
+ }
127
+ /**
128
+ * Analyse an already parsed message body.
129
+ *
130
+ * Never throws. A body that cannot be segmented is returned whole as a single visible `UNKNOWN`
131
+ * segment, because showing too much is always preferable to showing nothing.
132
+ */
133
+ async function analyze(content, options = {}) {
134
+ const config = options.confidence ?? confidence_1.DEFAULT_CONFIDENCE;
135
+ const warnings = [];
136
+ const text = typeof content.text === 'string' ? content.text : '';
137
+ const html = typeof content.html === 'string' ? content.html : '';
138
+ const isReply = Boolean(content.inReplyTo) || (content.references?.length ?? 0) > 0 || REPLY_SUBJECT.test(content.subject ?? '');
139
+ const base = {
140
+ originalAvailable: true,
141
+ extractorVersion: types_1.EXTRACTOR_VERSION,
142
+ isReply
143
+ };
144
+ if (!text.trim() && !html.trim()) {
145
+ return {
146
+ ...base,
147
+ segments: [],
148
+ hiddenSegments: [],
149
+ overallConfidence: 1,
150
+ warnings: [{ code: 'no_body_parts' }],
151
+ primaryRepresentation: 'none',
152
+ historyUsed: false
153
+ };
154
+ }
155
+ const senderTokens = senderTokensFrom(content.from);
156
+ let history = [];
157
+ try {
158
+ history = await resolveHistory(options);
159
+ }
160
+ catch {
161
+ // History is optional evidence. Failing to build it is never fatal.
162
+ }
163
+ try {
164
+ let textResult = null;
165
+ let htmlResult = null;
166
+ let primaryBlocks = [];
167
+ if (text.trim()) {
168
+ const segmented = (0, text_blocks_1.segmentTextBlocks)(text, content.textPartId ?? 'TEXT');
169
+ textResult = buildRepresentation(segmented.blocks, 'text', config, history, senderTokens);
170
+ primaryBlocks = segmented.blocks;
171
+ }
172
+ if (html.trim()) {
173
+ const segmented = (0, html_blocks_1.segmentHtmlBlocks)(html, content.htmlPartId ?? 'HTML');
174
+ if (segmented.degraded) {
175
+ warnings.push({ code: 'html_parse_error' });
176
+ }
177
+ htmlResult = buildRepresentation(segmented.blocks, 'html', config, history, senderTokens);
178
+ if (!textResult) {
179
+ primaryBlocks = segmented.blocks;
180
+ }
181
+ }
182
+ if (!textResult && !htmlResult) {
183
+ return {
184
+ ...base,
185
+ segments: [],
186
+ hiddenSegments: [],
187
+ overallConfidence: 1,
188
+ warnings: [...warnings, { code: 'no_body_parts' }],
189
+ primaryRepresentation: 'none',
190
+ historyUsed: false
191
+ };
192
+ }
193
+ const outcome = (0, reconcile_1.pickPrimary)(textResult, htmlResult);
194
+ if (outcome.primary === htmlResult && htmlResult) {
195
+ primaryBlocks = htmlResult.decisions.map(decision => decision.block);
196
+ }
197
+ else if (textResult) {
198
+ primaryBlocks = textResult.decisions.map(decision => decision.block);
199
+ }
200
+ const decisions = (0, reconcile_1.reconcile)(outcome);
201
+ const segments = toSegments(decisions, config);
202
+ const visible = segments.filter(segment => segment.visibility !== 'hide');
203
+ const hidden = segments.filter(segment => segment.visibility === 'hide');
204
+ // Invariant: a message with body text always yields something visible.
205
+ const bodyHasText = decisions.some(decision => decision.block.content.trim().length > 0);
206
+ if (bodyHasText && !visible.length) {
207
+ warnings.push({ code: 'extraction_degraded', detail: 'no visible segment survived classification' });
208
+ for (const segment of segments) {
209
+ segment.visibility = 'show';
210
+ segment.type = 'UNKNOWN';
211
+ segment.reasons.push('degraded_to_unknown');
212
+ }
213
+ return {
214
+ ...base,
215
+ segments,
216
+ hiddenSegments: [],
217
+ overallConfidence: 0.5,
218
+ warnings,
219
+ primaryRepresentation: outcome.primary.representation,
220
+ historyUsed: history.length > 0,
221
+ ...(options.explain ? { diagnostics: buildDiagnostics(primaryBlocks, decisions) } : {})
222
+ };
223
+ }
224
+ return {
225
+ ...base,
226
+ segments: visible,
227
+ hiddenSegments: hidden,
228
+ overallConfidence: (0, confidence_1.overallConfidence)(segments),
229
+ warnings,
230
+ primaryRepresentation: outcome.primary.representation,
231
+ historyUsed: history.length > 0,
232
+ ...(options.explain ? { diagnostics: buildDiagnostics(primaryBlocks, decisions) } : {})
233
+ };
234
+ }
235
+ catch (err) {
236
+ const fallback = text || html;
237
+ return {
238
+ ...base,
239
+ segments: fallback.trim()
240
+ ? [
241
+ {
242
+ type: 'UNKNOWN',
243
+ text: fallback,
244
+ confidence: 0.5,
245
+ visibility: 'show',
246
+ reasons: ['degraded_to_unknown'],
247
+ source: { mimePart: 'TEXT', representation: 'text', blockIndex: 0, start: 0, end: fallback.length }
248
+ }
249
+ ]
250
+ : [],
251
+ hiddenSegments: [],
252
+ overallConfidence: 0.5,
253
+ warnings: [...warnings, { code: 'extraction_degraded', detail: err instanceof Error ? err.message.slice(0, 200) : 'unknown' }],
254
+ primaryRepresentation: 'none',
255
+ historyUsed: history.length > 0
256
+ };
257
+ }
258
+ }
259
+ function buildDiagnostics(blocks, decisions) {
260
+ return {
261
+ primaryPart: blocks[0]?.mimePart ?? null,
262
+ blocks: decisions.map(decision => ({
263
+ index: decision.block.index,
264
+ kind: decision.block.kind,
265
+ label: decision.label,
266
+ confidence: decision.confidence,
267
+ reasons: decision.reasons,
268
+ evidence: decision.block.evidence.map(item => ({ id: item.id, kind: item.kind, strength: item.strength })),
269
+ features: decision.block.features,
270
+ preview: decision.block.content.slice(0, 120)
271
+ }))
272
+ };
273
+ }
274
+ //# sourceMappingURL=analyze.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.js","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AA0FH,4CAaC;AAqED,wCAkBC;AAQD,0BAuIC;AA3UD,uDAA0D;AAC1D,uDAA0D;AAE1D,4CAAkD;AAClD,oDAAyF;AACzF,sDAAoH;AACpH,6CAA2I;AAC3I,mCAAqF;AA4DrF,MAAM,aAAa,GAAG,6DAA6D,CAAC;AAEpF,SAAS,YAAY,CAAC,MAAwB;IAC1C,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,SAAS,CAAC,MAAwB;IACvC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CACvB,KAAK,CAAC,QAAQ,CAAC,IAAI,CACf,QAAQ,CAAC,EAAE,CACP,QAAQ,CAAC,IAAI,KAAK,cAAc,IAAI,QAAQ,CAAC,IAAI,KAAK,gBAAgB,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,iBAAiB,IAAI,QAAQ,CAAC,QAAQ,IAAI,IAAI,CAAC,CACnJ,CACJ,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,IAA+B;IAC5D,IAAI,CAAC,IAAI,EAAE,CAAC;QACR,OAAO,EAAE,CAAC;IACd,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACpE,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,GAAG,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACxG,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACpB,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACL,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,mBAAmB,CACxB,MAAe,EACf,cAA+B,EAC/B,MAAwB,EACxB,OAAmC,EACnC,YAA+B;IAE/B,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAA,qBAAW,EAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACnE,OAAO;QACH,cAAc;QACd,SAAS,EAAE,IAAA,sBAAc,EAAC,MAAM,EAAE,EAAE,MAAM,EAAE,qBAAqB,EAAE,IAAA,wBAAc,EAAC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;QAC3G,YAAY,EAAE,YAAY,CAAC,MAAM,CAAC;QAClC,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC;QAC5B,OAAO;KACV,CAAC;AACN,CAAC;AAED,oGAAoG;AACpG,SAAS,UAAU,CAAC,SAA4C,EAAE,MAAwB;IACtF,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC;QAC7B,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC/C,MAAM,UAAU,GAAG,IAAA,0BAAa,EAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAE9E,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,UAAU,KAAK,UAAU,IAAI,QAAQ,CAAC,MAAM,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;YACpI,QAAQ,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;YACrF,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;gBACb,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC;YACvD,CAAC;YACD,QAAQ,CAAC,MAAM,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;YAChC,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;YACzE,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;gBACpC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;oBACrC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAClC,CAAC;YACL,CAAC;YACD,SAAS;QACb,CAAC;QAED,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,IAAI,CAAC,QAAQ,EAAE,CAAC;YACzC,SAAS;QACb,CAAC;QAED,QAAQ,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,QAAQ,CAAC,KAAK;YACpB,IAAI,EAAE,KAAK,CAAC,OAAO;YACnB,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,UAAU;YACV,OAAO,EAAE,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC;YAC9B,MAAM,EAAE;gBACJ,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,cAAc,EAAE,KAAK,CAAC,cAAc;gBACpC,UAAU,EAAE,KAAK,CAAC,KAAK;gBACvB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACvD;SACJ,CAAC,CAAC;IACP,CAAC;IAED,OAAO,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACtE,CAAC;AAED,8EAA8E;AACvE,KAAK,UAAU,cAAc,CAAC,OAAuB;IACxD,IAAI,OAAO,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC;QACnC,OAAO,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACzC,CAAC;IACD,MAAM,SAAS,GAAsB,EAAE,CAAC;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACtD,MAAM,IAAI,GAAI,OAAO,CAAC,OAAyD,CAAC,CAAC,CAAC,CAAC;QACnF,IAAI,CAAC;YACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpD,SAAS,CAAC,IAAI,CAAC,MAAM,IAAA,8BAAoB,EAAC,IAAI,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC9E,CAAC;iBAAM,IAAI,IAAI,EAAE,CAAC;gBACd,SAAS,CAAC,IAAI,CAAC,IAAA,yCAA+B,EAAC,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC/G,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACL,uDAAuD;QAC3D,CAAC;IACL,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,OAAO,CAAC,OAAuB,EAAE,UAA0B,EAAE;IAC/E,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,IAAI,+BAAkB,CAAC;IACxD,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,MAAM,IAAI,GAAG,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IAEjI,MAAM,IAAI,GAAG;QACT,iBAAiB,EAAE,IAAa;QAChC,gBAAgB,EAAE,yBAAiB;QACnC,OAAO;KACV,CAAC;IAEF,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/B,OAAO;YACH,GAAG,IAAI;YACP,QAAQ,EAAE,EAAE;YACZ,cAAc,EAAE,EAAE;YAClB,iBAAiB,EAAE,CAAC;YACpB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;YACrC,qBAAqB,EAAE,MAAM;YAC7B,WAAW,EAAE,KAAK;SACrB,CAAC;IACN,CAAC;IAED,MAAM,YAAY,GAAG,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,OAAO,GAAsB,EAAE,CAAC;IACpC,IAAI,CAAC;QACD,OAAO,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACL,oEAAoE;IACxE,CAAC;IAED,IAAI,CAAC;QACD,IAAI,UAAU,GAAgC,IAAI,CAAC;QACnD,IAAI,UAAU,GAAgC,IAAI,CAAC;QACnD,IAAI,aAAa,GAAY,EAAE,CAAC;QAEhC,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACd,MAAM,SAAS,GAAG,IAAA,+BAAiB,EAAC,IAAI,EAAE,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,CAAC;YACxE,UAAU,GAAG,mBAAmB,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;YAC1F,aAAa,GAAG,SAAS,CAAC,MAAM,CAAC;QACrC,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACd,MAAM,SAAS,GAAG,IAAA,+BAAiB,EAAC,IAAI,EAAE,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,CAAC;YACxE,IAAI,SAAS,CAAC,QAAQ,EAAE,CAAC;gBACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,CAAC;YAChD,CAAC;YACD,UAAU,GAAG,mBAAmB,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;YAC1F,IAAI,CAAC,UAAU,EAAE,CAAC;gBACd,aAAa,GAAG,SAAS,CAAC,MAAM,CAAC;YACrC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,EAAE,CAAC;YAC7B,OAAO;gBACH,GAAG,IAAI;gBACP,QAAQ,EAAE,EAAE;gBACZ,cAAc,EAAE,EAAE;gBAClB,iBAAiB,EAAE,CAAC;gBACpB,QAAQ,EAAE,CAAC,GAAG,QAAQ,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;gBAClD,qBAAqB,EAAE,MAAM;gBAC7B,WAAW,EAAE,KAAK;aACrB,CAAC;QACN,CAAC;QAED,MAAM,OAAO,GAAG,IAAA,uBAAW,EAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QACpD,IAAI,OAAO,CAAC,OAAO,KAAK,UAAU,IAAI,UAAU,EAAE,CAAC;YAC/C,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACzE,CAAC;aAAM,IAAI,UAAU,EAAE,CAAC;YACpB,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACzE,CAAC;QAED,MAAM,SAAS,GAAG,IAAA,qBAAS,EAAC,OAAO,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,UAAU,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAE/C,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,UAAU,KAAK,MAAM,CAAC,CAAC;QAC1E,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,UAAU,KAAK,MAAM,CAAC,CAAC;QAEzE,uEAAuE;QACvE,MAAM,WAAW,GAAG,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACzF,IAAI,WAAW,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,MAAM,EAAE,4CAA4C,EAAE,CAAC,CAAC;YACrG,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC7B,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC;gBAC5B,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC;gBACzB,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;YAChD,CAAC;YACD,OAAO;gBACH,GAAG,IAAI;gBACP,QAAQ;gBACR,cAAc,EAAE,EAAE;gBAClB,iBAAiB,EAAE,GAAG;gBACtB,QAAQ;gBACR,qBAAqB,EAAE,OAAO,CAAC,OAAO,CAAC,cAAc;gBACrD,WAAW,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC;gBAC/B,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,gBAAgB,CAAC,aAAa,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1F,CAAC;QACN,CAAC;QAED,OAAO;YACH,GAAG,IAAI;YACP,QAAQ,EAAE,OAAO;YACjB,cAAc,EAAE,MAAM;YACtB,iBAAiB,EAAE,IAAA,8BAAiB,EAAC,QAAQ,CAAC;YAC9C,QAAQ;YACR,qBAAqB,EAAE,OAAO,CAAC,OAAO,CAAC,cAAc;YACrD,WAAW,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC;YAC/B,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,gBAAgB,CAAC,aAAa,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1F,CAAC;IACN,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACX,MAAM,QAAQ,GAAG,IAAI,IAAI,IAAI,CAAC;QAC9B,OAAO;YACH,GAAG,IAAI;YACP,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE;gBACrB,CAAC,CAAC;oBACI;wBACI,IAAI,EAAE,SAAS;wBACf,IAAI,EAAE,QAAQ;wBACd,UAAU,EAAE,GAAG;wBACf,UAAU,EAAE,MAAM;wBAClB,OAAO,EAAE,CAAC,qBAAqB,CAAC;wBAChC,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,MAAM,EAAE;qBACtG;iBACJ;gBACH,CAAC,CAAC,EAAE;YACR,cAAc,EAAE,EAAE;YAClB,iBAAiB,EAAE,GAAG;YACtB,QAAQ,EAAE,CAAC,GAAG,QAAQ,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;YAC9H,qBAAqB,EAAE,MAAM;YAC7B,WAAW,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC;SAClC,CAAC;IACN,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAwB,EAAE,SAA4C;IAC5F,OAAO;QACH,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,IAAI,IAAI;QACxC,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YAC/B,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK;YAC3B,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI;YACzB,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC1G,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,QAA8C;YACvE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;SAChD,CAAC,CAAC;KACN,CAAC;AACN,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Minimal OpenAI chat completions client.
3
+ *
4
+ * No SDK dependency: this project ships three runtime dependencies and none of them opens a socket.
5
+ * Adding a vendor SDK for one optional command would change that materially, and the surface used
6
+ * here is a single POST with a JSON schema response format.
7
+ *
8
+ * This is the one place in the codebase that sends message content off the machine. It is only
9
+ * reachable from `email-content annotate`, never from `extract()` or the corpus scanner. See
10
+ * docs/threat-model.md, "Configured exception: LLM annotation".
11
+ */
12
+ export interface OpenAIConfig {
13
+ apiKey: string;
14
+ baseUrl: string;
15
+ model: string;
16
+ /** Per request timeout in milliseconds. */
17
+ timeoutMs: number;
18
+ maxRetries: number;
19
+ }
20
+ export interface Usage {
21
+ promptTokens: number;
22
+ completionTokens: number;
23
+ totalTokens: number;
24
+ requests: number;
25
+ }
26
+ export interface CompletionRequest {
27
+ system: string;
28
+ user: string;
29
+ schemaName: string;
30
+ schema: Record<string, unknown>;
31
+ }
32
+ /**
33
+ * The narrow surface the annotator depends on.
34
+ *
35
+ * Depending on this rather than on the concrete client is what lets the annotator's validation
36
+ * logic be tested without a network call, which matters because that validation is the guard
37
+ * against a model returning indices that do not exist.
38
+ */
39
+ export interface CompletionClient {
40
+ readonly model: string;
41
+ complete<T>(request: CompletionRequest): Promise<T>;
42
+ }
43
+ export declare class OpenAIError extends Error {
44
+ readonly status: number;
45
+ readonly retryable: boolean;
46
+ constructor(message: string, status: number, retryable: boolean);
47
+ }
48
+ /** Load configuration from the environment, after process.loadEnvFile(). */
49
+ export declare function configFromEnv(model: string, overrides?: Partial<OpenAIConfig>): OpenAIConfig;
50
+ export declare class OpenAIClient implements CompletionClient {
51
+ private readonly config;
52
+ readonly usage: Usage;
53
+ constructor(config: OpenAIConfig);
54
+ get model(): string;
55
+ /**
56
+ * Request a structured completion and return the parsed object.
57
+ *
58
+ * Retries on rate limits and transient server errors with exponential backoff plus jitter. A
59
+ * schema violation is not retried: strict mode means the model already failed to satisfy the
60
+ * contract, and a retry would usually fail the same way.
61
+ */
62
+ complete<T>(request: CompletionRequest): Promise<T>;
63
+ }
64
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/annotate/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACvD;AAED,qBAAa,WAAY,SAAQ,KAAK;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;gBAEhB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO;CAMlE;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,GAAE,OAAO,CAAC,YAAY,CAAM,GAAG,YAAY,CAahG;AAMD,qBAAa,YAAa,YAAW,gBAAgB;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAyE;gBAElF,MAAM,EAAE,YAAY;IAIhC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED;;;;;;OAMG;IACG,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC,CAAC;CAoF5D"}
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal OpenAI chat completions client.
4
+ *
5
+ * No SDK dependency: this project ships three runtime dependencies and none of them opens a socket.
6
+ * Adding a vendor SDK for one optional command would change that materially, and the surface used
7
+ * here is a single POST with a JSON schema response format.
8
+ *
9
+ * This is the one place in the codebase that sends message content off the machine. It is only
10
+ * reachable from `email-content annotate`, never from `extract()` or the corpus scanner. See
11
+ * docs/threat-model.md, "Configured exception: LLM annotation".
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.OpenAIClient = exports.OpenAIError = void 0;
15
+ exports.configFromEnv = configFromEnv;
16
+ class OpenAIError extends Error {
17
+ status;
18
+ retryable;
19
+ constructor(message, status, retryable) {
20
+ super(message);
21
+ this.name = 'OpenAIError';
22
+ this.status = status;
23
+ this.retryable = retryable;
24
+ }
25
+ }
26
+ exports.OpenAIError = OpenAIError;
27
+ /** Load configuration from the environment, after process.loadEnvFile(). */
28
+ function configFromEnv(model, overrides = {}) {
29
+ const apiKey = process.env.OPENAI_API_KEY;
30
+ if (!apiKey) {
31
+ throw new Error('OPENAI_API_KEY is not set. Copy .env.example to .env and fill it in.');
32
+ }
33
+ return {
34
+ apiKey,
35
+ baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
36
+ model,
37
+ timeoutMs: 120000,
38
+ maxRetries: 5,
39
+ ...overrides
40
+ };
41
+ }
42
+ function sleep(ms) {
43
+ return new Promise(resolve => setTimeout(resolve, ms));
44
+ }
45
+ class OpenAIClient {
46
+ config;
47
+ usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0, requests: 0 };
48
+ constructor(config) {
49
+ this.config = config;
50
+ }
51
+ get model() {
52
+ return this.config.model;
53
+ }
54
+ /**
55
+ * Request a structured completion and return the parsed object.
56
+ *
57
+ * Retries on rate limits and transient server errors with exponential backoff plus jitter. A
58
+ * schema violation is not retried: strict mode means the model already failed to satisfy the
59
+ * contract, and a retry would usually fail the same way.
60
+ */
61
+ async complete(request) {
62
+ let lastError = null;
63
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
64
+ if (attempt > 0) {
65
+ // Full jitter, capped, so a burst of workers does not retry in lockstep.
66
+ const backoff = Math.min(30000, 500 * 2 ** (attempt - 1));
67
+ await sleep(Math.random() * backoff);
68
+ }
69
+ const controller = new AbortController();
70
+ const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
71
+ try {
72
+ const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
73
+ method: 'POST',
74
+ signal: controller.signal,
75
+ headers: {
76
+ Authorization: `Bearer ${this.config.apiKey}`,
77
+ 'Content-Type': 'application/json'
78
+ },
79
+ body: JSON.stringify({
80
+ model: this.config.model,
81
+ messages: [
82
+ { role: 'system', content: request.system },
83
+ { role: 'user', content: request.user }
84
+ ],
85
+ response_format: {
86
+ type: 'json_schema',
87
+ json_schema: { name: request.schemaName, strict: true, schema: request.schema }
88
+ }
89
+ })
90
+ });
91
+ if (!response.ok) {
92
+ const retryable = response.status === 429 || response.status >= 500;
93
+ let detail = `HTTP ${response.status}`;
94
+ try {
95
+ const body = (await response.json());
96
+ if (body.error?.message) {
97
+ detail = body.error.message.slice(0, 300);
98
+ }
99
+ }
100
+ catch {
101
+ // Body was not JSON. The status alone is enough.
102
+ }
103
+ const error = new OpenAIError(detail, response.status, retryable);
104
+ if (!retryable) {
105
+ throw error;
106
+ }
107
+ lastError = error;
108
+ continue;
109
+ }
110
+ const body = (await response.json());
111
+ this.usage.requests++;
112
+ this.usage.promptTokens += body.usage?.prompt_tokens ?? 0;
113
+ this.usage.completionTokens += body.usage?.completion_tokens ?? 0;
114
+ this.usage.totalTokens += body.usage?.total_tokens ?? 0;
115
+ const choice = body.choices?.[0];
116
+ if (choice?.finish_reason === 'length') {
117
+ throw new OpenAIError('response truncated by token limit', 0, false);
118
+ }
119
+ const content = choice?.message?.content;
120
+ if (!content) {
121
+ throw new OpenAIError('empty completion', 0, false);
122
+ }
123
+ return JSON.parse(content);
124
+ }
125
+ catch (err) {
126
+ if (err instanceof OpenAIError && !err.retryable) {
127
+ throw err;
128
+ }
129
+ lastError = err instanceof Error ? err : new Error(String(err));
130
+ }
131
+ finally {
132
+ clearTimeout(timer);
133
+ }
134
+ }
135
+ throw lastError ?? new Error('request failed');
136
+ }
137
+ }
138
+ exports.OpenAIClient = OpenAIClient;
139
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/annotate/client.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAkDH,sCAaC;AA1BD,MAAa,WAAY,SAAQ,KAAK;IACzB,MAAM,CAAS;IACf,SAAS,CAAU;IAE5B,YAAY,OAAe,EAAE,MAAc,EAAE,SAAkB;QAC3D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAVD,kCAUC;AAED,4EAA4E;AAC5E,SAAgB,aAAa,CAAC,KAAa,EAAE,YAAmC,EAAE;IAC9E,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAC;IAC5F,CAAC;IACD,OAAO;QACH,MAAM;QACN,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,2BAA2B;QACnE,KAAK;QACL,SAAS,EAAE,MAAM;QACjB,UAAU,EAAE,CAAC;QACb,GAAG,SAAS;KACf,CAAC;AACN,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACrB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAa,YAAY;IACJ,MAAM,CAAe;IAC7B,KAAK,GAAU,EAAE,YAAY,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IAE9F,YAAY,MAAoB;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAED,IAAI,KAAK;QACL,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IAC7B,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,QAAQ,CAAI,OAA0B;QACxC,IAAI,SAAS,GAAiB,IAAI,CAAC;QAEnC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;YACjE,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBACd,yEAAyE;gBACzE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;gBAC1D,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC;YACzC,CAAC;YAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAE1E,IAAI,CAAC;gBACD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,mBAAmB,EAAE;oBACpE,MAAM,EAAE,MAAM;oBACd,MAAM,EAAE,UAAU,CAAC,MAAM;oBACzB,OAAO,EAAE;wBACL,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;wBAC7C,cAAc,EAAE,kBAAkB;qBACrC;oBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACjB,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK;wBACxB,QAAQ,EAAE;4BACN,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE;4BAC3C,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE;yBAC1C;wBACD,eAAe,EAAE;4BACb,IAAI,EAAE,aAAa;4BACnB,WAAW,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE;yBAClF;qBACJ,CAAC;iBACL,CAAC,CAAC;gBAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;oBACf,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,CAAC;oBACpE,IAAI,MAAM,GAAG,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC;oBACvC,IAAI,CAAC;wBACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAqC,CAAC;wBACzE,IAAI,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;4BACtB,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;wBAC9C,CAAC;oBACL,CAAC;oBAAC,MAAM,CAAC;wBACL,iDAAiD;oBACrD,CAAC;oBACD,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;oBAClE,IAAI,CAAC,SAAS,EAAE,CAAC;wBACb,MAAM,KAAK,CAAC;oBAChB,CAAC;oBACD,SAAS,GAAG,KAAK,CAAC;oBAClB,SAAS;gBACb,CAAC;gBAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAGlC,CAAC;gBAEF,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACtB,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,KAAK,EAAE,aAAa,IAAI,CAAC,CAAC;gBAC1D,IAAI,CAAC,KAAK,CAAC,gBAAgB,IAAI,IAAI,CAAC,KAAK,EAAE,iBAAiB,IAAI,CAAC,CAAC;gBAClE,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,KAAK,EAAE,YAAY,IAAI,CAAC,CAAC;gBAExD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;gBACjC,IAAI,MAAM,EAAE,aAAa,KAAK,QAAQ,EAAE,CAAC;oBACrC,MAAM,IAAI,WAAW,CAAC,mCAAmC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;gBACzE,CAAC;gBACD,MAAM,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC;gBACzC,IAAI,CAAC,OAAO,EAAE,CAAC;oBACX,MAAM,IAAI,WAAW,CAAC,kBAAkB,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;gBACxD,CAAC;gBACD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAM,CAAC;YACpC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACX,IAAI,GAAG,YAAY,WAAW,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;oBAC/C,MAAM,GAAG,CAAC;gBACd,CAAC;gBACD,SAAS,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACpE,CAAC;oBAAS,CAAC;gBACP,YAAY,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC;QACL,CAAC;QAED,MAAM,SAAS,IAAI,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACnD,CAAC;CACJ;AAvGD,oCAuGC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * LLM block annotator.
3
+ *
4
+ * Produces labels for an already segmented message. It does not replace the segmenter: the model
5
+ * only ever sees blocks the deterministic pipeline produced, and only ever returns labels for those
6
+ * block indices. That keeps the annotation aligned with the pipeline it is meant to evaluate, and
7
+ * keeps generated text out of the system entirely.
8
+ *
9
+ * These labels are weak supervision, not ground truth. They are stored with their model and prompt
10
+ * version, kept separate from human labels, and are meant for bootstrapping, disagreement mining
11
+ * and sampling, exactly as the project's plan requires of any non human label source.
12
+ */
13
+ import { type Block } from '../segment/types';
14
+ import { type Label } from '../types';
15
+ import { type CompletionClient } from './client';
16
+ /** Characters of each block sent to the model. Enough for a decision, bounded for cost. */
17
+ export declare const MAX_BLOCK_CHARS = 700;
18
+ /** Blocks per message. Longer messages are truncated with a recorded flag. */
19
+ export declare const MAX_BLOCKS = 60;
20
+ export interface LlmLabel {
21
+ index: number;
22
+ label: Label;
23
+ confidence: number;
24
+ }
25
+ export interface AnnotationResult {
26
+ labels: LlmLabel[];
27
+ model: string;
28
+ promptVersion: string;
29
+ /** Blocks that were sent but came back unlabelled, defaulted to UNKNOWN. */
30
+ missing: number[];
31
+ /** Indices the model invented that do not exist. Discarded. */
32
+ spurious: number[];
33
+ blocksSent: number;
34
+ blocksTotal: number;
35
+ }
36
+ /** Blocks worth spending a token on. Blank blocks carry no decision. */
37
+ export declare function annotatableBlocks(blocks: readonly Block[]): Block[];
38
+ /**
39
+ * Annotate one message's blocks.
40
+ *
41
+ * @param client configured OpenAI client
42
+ * @param blocks blocks from the primary representation
43
+ * @param isReply whether the message carries threading headers
44
+ */
45
+ export declare function annotateBlocks(client: CompletionClient, blocks: readonly Block[], isReply: boolean): Promise<AnnotationResult>;
46
+ //# sourceMappingURL=llm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../src/annotate/llm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD,2FAA2F;AAC3F,eAAO,MAAM,eAAe,MAAM,CAAC;AAEnC,8EAA8E;AAC9E,eAAO,MAAM,UAAU,KAAK,CAAC;AAE7B,MAAM,WAAW,QAAQ;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,KAAK,EAAE,CAEnE;AAkBD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,SAAS,KAAK,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgEpI"}
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * LLM block annotator.
4
+ *
5
+ * Produces labels for an already segmented message. It does not replace the segmenter: the model
6
+ * only ever sees blocks the deterministic pipeline produced, and only ever returns labels for those
7
+ * block indices. That keeps the annotation aligned with the pipeline it is meant to evaluate, and
8
+ * keeps generated text out of the system entirely.
9
+ *
10
+ * These labels are weak supervision, not ground truth. They are stored with their model and prompt
11
+ * version, kept separate from human labels, and are meant for bootstrapping, disagreement mining
12
+ * and sampling, exactly as the project's plan requires of any non human label source.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.MAX_BLOCKS = exports.MAX_BLOCK_CHARS = void 0;
16
+ exports.annotatableBlocks = annotatableBlocks;
17
+ exports.annotateBlocks = annotateBlocks;
18
+ const prompt_1 = require("./prompt");
19
+ /** Characters of each block sent to the model. Enough for a decision, bounded for cost. */
20
+ exports.MAX_BLOCK_CHARS = 700;
21
+ /** Blocks per message. Longer messages are truncated with a recorded flag. */
22
+ exports.MAX_BLOCKS = 60;
23
+ /** Blocks worth spending a token on. Blank blocks carry no decision. */
24
+ function annotatableBlocks(blocks) {
25
+ return blocks.filter(block => block.content.trim().length > 0);
26
+ }
27
+ function toPromptBlocks(blocks) {
28
+ return blocks.slice(0, exports.MAX_BLOCKS).map(block => {
29
+ const text = block.content.trim();
30
+ const truncated = text.length > exports.MAX_BLOCK_CHARS;
31
+ return {
32
+ index: block.index,
33
+ text: truncated ? `${text.slice(0, exports.MAX_BLOCK_CHARS)}...` : text,
34
+ truncated,
35
+ // Both representations reduce to one nesting number: ">" depth in plain text,
36
+ // blockquote nesting in HTML. Without it the annotator cannot tell a quoted greeting
37
+ // from a freshly written one.
38
+ quoteDepth: Math.max(block.features.quoteDepth, block.features.blockquoteDepth)
39
+ };
40
+ });
41
+ }
42
+ /**
43
+ * Annotate one message's blocks.
44
+ *
45
+ * @param client configured OpenAI client
46
+ * @param blocks blocks from the primary representation
47
+ * @param isReply whether the message carries threading headers
48
+ */
49
+ async function annotateBlocks(client, blocks, isReply) {
50
+ const candidates = annotatableBlocks(blocks);
51
+ const sent = toPromptBlocks(candidates);
52
+ if (!sent.length) {
53
+ return {
54
+ labels: [],
55
+ model: client.model,
56
+ promptVersion: prompt_1.PROMPT_VERSION,
57
+ missing: [],
58
+ spurious: [],
59
+ blocksSent: 0,
60
+ blocksTotal: candidates.length
61
+ };
62
+ }
63
+ const response = await client.complete({
64
+ system: prompt_1.SYSTEM_PROMPT,
65
+ user: (0, prompt_1.renderUserPrompt)(sent, isReply),
66
+ schemaName: 'block_labels',
67
+ schema: prompt_1.RESPONSE_SCHEMA
68
+ });
69
+ // Validate against the blocks we actually sent. The schema constrains shape and label values,
70
+ // but nothing stops the model returning an index we never asked about, or omitting one.
71
+ const expected = new Set(sent.map(block => block.index));
72
+ const seen = new Set();
73
+ const labels = [];
74
+ const spurious = [];
75
+ for (const item of response.labels ?? []) {
76
+ if (!expected.has(item.index) || seen.has(item.index)) {
77
+ spurious.push(item.index);
78
+ continue;
79
+ }
80
+ seen.add(item.index);
81
+ labels.push({
82
+ index: item.index,
83
+ label: item.label,
84
+ confidence: Number.isFinite(item.confidence) ? Math.min(1, Math.max(0, item.confidence)) : 0.5
85
+ });
86
+ }
87
+ // A block the model declined to label is UNKNOWN, which stays visible. Silently dropping it
88
+ // would let an omission look like agreement later.
89
+ const missing = [];
90
+ for (const index of expected) {
91
+ if (!seen.has(index)) {
92
+ missing.push(index);
93
+ labels.push({ index, label: 'UNKNOWN', confidence: 0 });
94
+ }
95
+ }
96
+ labels.sort((a, b) => a.index - b.index);
97
+ return {
98
+ labels,
99
+ model: client.model,
100
+ promptVersion: prompt_1.PROMPT_VERSION,
101
+ missing,
102
+ spurious,
103
+ blocksSent: sent.length,
104
+ blocksTotal: candidates.length
105
+ };
106
+ }
107
+ //# sourceMappingURL=llm.js.map