@devilsdev/rag-pipeline-utils 2.6.2 → 2.7.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 (90) hide show
  1. package/BENCHMARKS.md +7 -1
  2. package/CHANGELOG.md +121 -0
  3. package/README.md +35 -24
  4. package/dist/index.cjs +4164 -1825
  5. package/dist/index.d.ts +265 -256
  6. package/dist/index.mjs +1 -1
  7. package/dist/types/cache/memory-cache.d.ts +4 -1
  8. package/dist/types/cache/redis-cache.d.ts +4 -1
  9. package/dist/types/cli/services/instrument.d.ts +14 -0
  10. package/dist/types/cli/services/rag-service.d.ts +140 -0
  11. package/dist/types/cli/shared/config-context.d.ts +9 -0
  12. package/dist/types/config/component-factory.d.ts +57 -0
  13. package/dist/types/config/enhanced-ragrc-schema.d.ts +648 -0
  14. package/dist/types/config/load-config.d.ts +18 -0
  15. package/dist/types/config/normalize-config.d.ts +9 -0
  16. package/dist/types/core/observability/event-logger.d.ts +211 -0
  17. package/dist/types/core/observability/metrics.d.ts +585 -0
  18. package/dist/types/core/performance/parallel-processor.d.ts +92 -0
  19. package/dist/types/core/plugin-registry.d.ts +5 -0
  20. package/dist/types/dag/core/execution-scheduler.d.ts +3 -1
  21. package/dist/types/dag/dag-engine.d.ts +15 -2
  22. package/dist/types/dev/dev-server.d.ts +136 -0
  23. package/dist/types/dev/hot-reload.d.ts +226 -0
  24. package/dist/types/ecosystem/plugin-analytics-dashboard.d.ts +141 -0
  25. package/dist/types/ecosystem/plugin-hub.d.ts +161 -0
  26. package/dist/types/ecosystem/plugin-installer.d.ts +40 -0
  27. package/dist/types/ecosystem/plugin-probe.d.ts +21 -0
  28. package/dist/types/ecosystem/plugin-scanner.d.ts +30 -0
  29. package/dist/types/ecosystem/tar-extract.d.ts +24 -0
  30. package/dist/types/enterprise/audit-logging.d.ts +217 -0
  31. package/dist/types/evaluate/compare.d.ts +140 -0
  32. package/dist/types/evaluate/dataset.d.ts +43 -0
  33. package/dist/types/evaluate/evaluator.d.ts +14 -0
  34. package/dist/types/evaluate/report.d.ts +56 -0
  35. package/dist/types/evaluate/synthetic.d.ts +64 -0
  36. package/dist/types/loader/csv-loader.d.ts +10 -0
  37. package/dist/types/loader/document-reader.d.ts +18 -0
  38. package/dist/types/loader/html-loader.d.ts +21 -0
  39. package/dist/types/loader/markdown-loader.d.ts +21 -0
  40. package/dist/types/retrieval/bm25-search.d.ts +4 -0
  41. package/dist/types/retrieval/file-vector-store.d.ts +4 -1
  42. package/dist/types/retrieval/hybrid-retriever.d.ts +4 -0
  43. package/dist/types/retrieval/pgvector-retriever.d.ts +123 -0
  44. package/dist/types/retrieval/qdrant-retriever.d.ts +106 -0
  45. package/dist/types/security/validator.d.ts +11 -1
  46. package/dist/types/testing.d.ts +2 -0
  47. package/dist/types/utils/batch-processor.d.ts +205 -0
  48. package/dist/types/utils/connection-pool.d.ts +158 -0
  49. package/dist/types/utils/error-formatter.d.ts +312 -0
  50. package/dist/types/utils/logger.d.ts +29 -4
  51. package/dist/types/utils/rate-limiter.d.ts +13 -8
  52. package/dist/types/utils/retry.d.ts +10 -0
  53. package/package.json +12 -2
  54. package/src/cache/memory-cache.js +4 -1
  55. package/src/cache/redis-cache.js +4 -1
  56. package/src/cli/enhanced-cli-commands.js +79 -0
  57. package/src/cli/handlers/ai-handler.js +2 -2
  58. package/src/cli/handlers/eval-handler.js +201 -0
  59. package/src/cli/handlers/generate-handler.js +103 -0
  60. package/src/cli/services/benchmark.js +2 -2
  61. package/src/cli/services/rag-service.js +59 -10
  62. package/src/cli/shared/cli-utils.js +40 -1
  63. package/src/config/component-factory.js +32 -0
  64. package/src/connectors/local-embedder.js +20 -13
  65. package/src/core/observability/event-logger.js +135 -1
  66. package/src/core/observability/metrics.js +223 -5
  67. package/src/core/performance/parallel-processor.js +2 -0
  68. package/src/core/plugin-registry.js +5 -1
  69. package/src/dag/core/execution-scheduler.js +2 -0
  70. package/src/dag/dag-engine.js +13 -0
  71. package/src/dev/dev-server.js +6 -0
  72. package/src/ecosystem/plugin-hub.js +4 -0
  73. package/src/enterprise/audit-logging.js +5 -0
  74. package/src/evaluate/compare.js +395 -0
  75. package/src/evaluate/dataset.js +207 -0
  76. package/src/evaluate/report.js +350 -0
  77. package/src/evaluate/synthetic.js +246 -0
  78. package/src/index.js +17 -0
  79. package/src/retrieval/bm25-search.js +4 -0
  80. package/src/retrieval/file-vector-store.js +4 -1
  81. package/src/retrieval/hybrid-retriever.js +4 -0
  82. package/src/retrieval/pgvector-retriever.js +420 -0
  83. package/src/retrieval/qdrant-retriever.js +361 -0
  84. package/src/security/validator.js +8 -0
  85. package/src/testing.js +2 -0
  86. package/src/utils/batch-processor.js +7 -3
  87. package/src/utils/error-formatter.js +48 -20
  88. package/src/utils/logger.js +31 -1
  89. package/src/utils/rate-limiter.js +9 -4
  90. package/src/utils/retry.js +10 -1
@@ -0,0 +1,361 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Retriever on Qdrant, through its REST API with the built-in fetch: no
5
+ * client package is needed.
6
+ *
7
+ * Qdrant point ids must be integers or UUIDs, so each document id is
8
+ * mapped to a UUID derived from it, and the original id is kept in the
9
+ * payload (`rag_id`) with `content` and `metadata`. Writes wait for Qdrant
10
+ * to apply them (`wait=true`), so a query right after ingest sees them.
11
+ * Written against the Qdrant 1.20 API (`/points/query`).
12
+ *
13
+ * The collection can change without this process knowing, so the
14
+ * retriever has no cache identity unless you give it a `cacheKey`.
15
+ */
16
+
17
+ const crypto = require('crypto');
18
+ const { throwIfAborted } = require('../connectors/connector-utils');
19
+
20
+ const COLLECTION = /^[A-Za-z0-9_-]{1,255}$/;
21
+ const DEFAULT_COLLECTION = 'rag_documents';
22
+ const PAGE = 256;
23
+ const UPSERT_BATCH = 256;
24
+
25
+ /** A stable UUID (version 8, RFC 9562) for a document id. */
26
+ function pointId(id) {
27
+ const h = crypto.createHash('sha256').update(String(id)).digest();
28
+ h[6] = (h[6] & 0x0f) | 0x80;
29
+ h[8] = (h[8] & 0x3f) | 0x80;
30
+ const x = h.subarray(0, 16).toString('hex');
31
+ return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;
32
+ }
33
+
34
+ function checkVector(vector, id) {
35
+ if (!Array.isArray(vector) || vector.length === 0) {
36
+ throw new Error(`QdrantRetriever: document "${id}" has no vector`);
37
+ }
38
+ if (!vector.every((x) => typeof x === 'number' && Number.isFinite(x))) {
39
+ throw new Error(
40
+ `QdrantRetriever: document "${id}" has a vector with a non-finite value`,
41
+ );
42
+ }
43
+ }
44
+
45
+ // Matches every point this class wrote, and nothing else in the collection.
46
+ const OURS = { must_not: [{ is_empty: { key: 'rag_id' } }] };
47
+
48
+ /**
49
+ * Retriever on Qdrant over its REST API, with no client package. Creates
50
+ * the collection on first store; similarity is cosine.
51
+ */
52
+ class QdrantRetriever {
53
+ /**
54
+ * @param {Object} [options]
55
+ * @param {string} [options.url='http://localhost:6333'] - Qdrant's REST endpoint
56
+ * @param {string} [options.apiKey] - Sent as the `api-key` header
57
+ * @param {string} [options.collection='rag_documents']
58
+ * @param {number} [options.dimensions] - Vector size for a new collection; defaults to the first stored vector's
59
+ * @param {boolean} [options.createCollection=true] - Create the collection on first store
60
+ * @param {string} [options.cacheKey] - Cache identity; change it when the collection changes
61
+ * @param {Function} [options.fetch] - fetch implementation (defaults to the global)
62
+ */
63
+ constructor(options = {}) {
64
+ const {
65
+ url = 'http://localhost:6333',
66
+ apiKey,
67
+ collection = DEFAULT_COLLECTION,
68
+ dimensions,
69
+ createCollection = true,
70
+ cacheKey,
71
+ fetch: fetchImpl = globalThis.fetch,
72
+ } = options;
73
+ if (!COLLECTION.test(collection)) {
74
+ throw new Error(
75
+ `QdrantRetriever: invalid collection name "${collection}" (letters, digits, "_" and "-")`,
76
+ );
77
+ }
78
+ if (
79
+ dimensions !== undefined &&
80
+ !(Number.isInteger(dimensions) && dimensions > 0)
81
+ ) {
82
+ throw new Error(
83
+ 'QdrantRetriever: `dimensions` must be a positive integer',
84
+ );
85
+ }
86
+ if (typeof fetchImpl !== 'function') {
87
+ throw new Error('QdrantRetriever needs fetch (Node.js 18 or later)');
88
+ }
89
+ this.url = String(url).replace(/\/+$/, '');
90
+ this.collection = collection;
91
+ this.dimensions = dimensions;
92
+ this.createCollection = createCollection;
93
+ this.cacheKey = cacheKey;
94
+ this._apiKey = apiKey;
95
+ this._fetch = fetchImpl;
96
+ this._ready = null;
97
+ }
98
+
99
+ /**
100
+ * Cache identity for createRagPipeline({ cache }): none unless you set
101
+ * `cacheKey`, since other clients can change the collection.
102
+ * @param {'retriever'} role
103
+ * @returns {string|null}
104
+ */
105
+ getCacheKey(role) {
106
+ if (role !== 'retriever' || !this.cacheKey) return null;
107
+ return `qdrant|${this.url}|${this.collection}|${this.cacheKey}`;
108
+ }
109
+
110
+ /**
111
+ * One REST call to the collection; resolves Qdrant's `result`.
112
+ * @private
113
+ * @param {string} method
114
+ * @param {string} path - After /collections/<name>
115
+ * @param {Object} [body]
116
+ * @param {{ signal?: AbortSignal, allow404?: boolean }} [options] - allow404 resolves null for a missing collection
117
+ * @returns {Promise<any>}
118
+ */
119
+ async _request(method, path, body, { signal, allow404 = false } = {}) {
120
+ throwIfAborted(signal);
121
+ const headers = {
122
+ 'content-type': 'application/json',
123
+ // fetch asks for gzip by default, and Qdrant's compressed responses
124
+ // stall each request by about 40 ms; uncompressed they take ~1 ms.
125
+ 'accept-encoding': 'identity',
126
+ };
127
+ if (this._apiKey) headers['api-key'] = this._apiKey;
128
+ let res;
129
+ try {
130
+ res = await this._fetch(
131
+ `${this.url}/collections/${this.collection}${path}`,
132
+ {
133
+ method,
134
+ headers,
135
+ ...(body !== undefined && { body: JSON.stringify(body) }),
136
+ ...(signal && { signal }),
137
+ },
138
+ );
139
+ } catch (err) {
140
+ throwIfAborted(signal);
141
+ throw new Error(
142
+ `QdrantRetriever: cannot reach Qdrant at ${this.url} (${err.message})`,
143
+ );
144
+ }
145
+ if (res.status === 404 && allow404) return null;
146
+ const text = await res.text();
147
+ let json;
148
+ try {
149
+ json = text ? JSON.parse(text) : {};
150
+ } catch {
151
+ json = {};
152
+ }
153
+ if (!res.ok) {
154
+ const detail =
155
+ (json.status && json.status.error) || text || res.statusText;
156
+ if (res.status === 401 || res.status === 403) {
157
+ throw new Error(
158
+ `QdrantRetriever: Qdrant refused the request (${res.status}); check QDRANT_API_KEY`,
159
+ );
160
+ }
161
+ throw new Error(
162
+ `QdrantRetriever: ${method} ${path || '/'} failed (${res.status}): ${detail}`,
163
+ );
164
+ }
165
+ return json.result;
166
+ }
167
+
168
+ _ensureCollection(dimensions) {
169
+ if (!this.createCollection) return Promise.resolve();
170
+ if (!this._ready) {
171
+ this._ready = (async () => {
172
+ const existing = await this._request('GET', '', undefined, {
173
+ allow404: true,
174
+ });
175
+ if (!existing) {
176
+ await this._request('PUT', '', {
177
+ vectors: {
178
+ size: this.dimensions || dimensions,
179
+ distance: 'Cosine',
180
+ },
181
+ });
182
+ }
183
+ // removeBySource filters on metadata.source; index it.
184
+ await this._request('PUT', '/index?wait=true', {
185
+ field_name: 'metadata.source',
186
+ field_schema: 'keyword',
187
+ });
188
+ })().catch((err) => {
189
+ this._ready = null;
190
+ throw err;
191
+ });
192
+ }
193
+ return this._ready;
194
+ }
195
+
196
+ /**
197
+ * Inserts or replaces documents.
198
+ * @param {Array<{ id: string, vector: number[], content?: string, metadata?: Object }>} docs
199
+ * @returns {Promise<void>}
200
+ */
201
+ async store(docs) {
202
+ if (!Array.isArray(docs))
203
+ throw new Error('QdrantRetriever.store() takes an array');
204
+ if (docs.length === 0) return;
205
+ let dimensions = null;
206
+ const points = docs.map((doc) => {
207
+ const id = doc && doc.id;
208
+ if (typeof id !== 'string' || id.length === 0) {
209
+ throw new Error(
210
+ 'QdrantRetriever.store(): every document needs a string id',
211
+ );
212
+ }
213
+ checkVector(doc.vector, id);
214
+ if (dimensions === null) dimensions = doc.vector.length;
215
+ else if (doc.vector.length !== dimensions) {
216
+ throw new Error(
217
+ `QdrantRetriever.store(): document "${id}" has ${doc.vector.length} dimensions, the batch has ${dimensions}`,
218
+ );
219
+ }
220
+ return {
221
+ id: pointId(id),
222
+ vector: doc.vector,
223
+ payload: {
224
+ rag_id: id,
225
+ content: doc.content || '',
226
+ metadata: doc.metadata || {},
227
+ },
228
+ };
229
+ });
230
+ await this._ensureCollection(dimensions);
231
+ for (let i = 0; i < points.length; i += UPSERT_BATCH) {
232
+ await this._request('PUT', '/points?wait=true', {
233
+ points: points.slice(i, i + UPSERT_BATCH),
234
+ });
235
+ }
236
+ }
237
+
238
+ /**
239
+ * The documents most similar to a query vector.
240
+ * @param {number[]|{ queryVector: number[], topK?: number, signal?: AbortSignal }} request
241
+ * @param {number} [k=10] - Results when `request` is a vector
242
+ * @returns {Promise<Array<{ id: string, score: number, content: string, metadata: Object }>>}
243
+ */
244
+ async retrieve(request, k = 10) {
245
+ const {
246
+ queryVector,
247
+ topK = k,
248
+ signal,
249
+ } = Array.isArray(request) ? { queryVector: request } : request || {};
250
+ if (!Number.isInteger(topK) || topK < 1) {
251
+ throw new Error(
252
+ 'QdrantRetriever.retrieve(): topK must be a positive integer',
253
+ );
254
+ }
255
+ checkVector(queryVector, 'query');
256
+ const result = await this._request(
257
+ 'POST',
258
+ '/points/query',
259
+ { query: queryVector, limit: topK, with_payload: true, filter: OURS },
260
+ { signal, allow404: true },
261
+ );
262
+ throwIfAborted(signal);
263
+ return ((result && result.points) || []).map((p) => ({
264
+ id: p.payload.rag_id,
265
+ score: p.score,
266
+ content: p.payload.content || '',
267
+ metadata: p.payload.metadata || {},
268
+ }));
269
+ }
270
+
271
+ /**
272
+ * Deletes the chunks of one source (`metadata.source`), for re-ingesting.
273
+ * @param {string} source
274
+ */
275
+ async removeBySource(source) {
276
+ await this._request(
277
+ 'POST',
278
+ '/points/delete?wait=true',
279
+ {
280
+ filter: {
281
+ must: [{ key: 'metadata.source', match: { value: String(source) } }],
282
+ },
283
+ },
284
+ { allow404: true },
285
+ );
286
+ }
287
+
288
+ /**
289
+ * Deletes documents by id.
290
+ * @param {string[]} ids
291
+ */
292
+ async delete(ids) {
293
+ await this._request(
294
+ 'POST',
295
+ '/points/delete?wait=true',
296
+ { points: ids.map(pointId) },
297
+ { allow404: true },
298
+ );
299
+ }
300
+
301
+ /** Deletes every document this class stored; the collection stays. */
302
+ async clear() {
303
+ await this._request(
304
+ 'POST',
305
+ '/points/delete?wait=true',
306
+ { filter: OURS },
307
+ { allow404: true },
308
+ );
309
+ }
310
+
311
+ /** @returns {Promise<number>} */
312
+ async size() {
313
+ const result = await this._request(
314
+ 'POST',
315
+ '/points/count',
316
+ { exact: true, filter: OURS },
317
+ { allow404: true },
318
+ );
319
+ return result ? result.count : 0;
320
+ }
321
+
322
+ /**
323
+ * Every document, without vectors.
324
+ * @param {{ limit?: number }} [options]
325
+ * @returns {Promise<Array<{ id: string, content: string, metadata: Object }>>}
326
+ */
327
+ async list({ limit } = {}) {
328
+ const out = [];
329
+ let offset;
330
+ do {
331
+ const result = await this._request(
332
+ 'POST',
333
+ '/points/scroll',
334
+ {
335
+ limit: limit ? Math.min(PAGE, limit - out.length) : PAGE,
336
+ with_payload: true,
337
+ with_vector: false,
338
+ filter: OURS,
339
+ ...(offset !== undefined && offset !== null && { offset }),
340
+ },
341
+ { allow404: true },
342
+ );
343
+ if (!result) break;
344
+ for (const p of result.points) {
345
+ out.push({
346
+ id: p.payload.rag_id,
347
+ content: p.payload.content || '',
348
+ metadata: p.payload.metadata || {},
349
+ });
350
+ }
351
+ offset = result.next_page_offset;
352
+ } while (
353
+ offset !== undefined &&
354
+ offset !== null &&
355
+ (!limit || out.length < limit)
356
+ );
357
+ return out;
358
+ }
359
+ }
360
+
361
+ module.exports = { QdrantRetriever, pointId };
@@ -1,5 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * Checks input against a maximum length and a fixed list of disallowed
5
+ * tokens (code fences, `${`, `</script`, `--`, `;`, `||`, `&&`).
6
+ * @param {*} input - Converted to a string
7
+ * @param {Object} [opts]
8
+ * @param {number} [opts.maxLength=20000]
9
+ * @returns {{valid: boolean, reasons: string[], sanitized: string}} `sanitized` is the input cut to `maxLength`
10
+ */
3
11
  function validateInput(input, opts = {}) {
4
12
  const maxLength = opts.maxLength || 20000;
5
13
  const inputStr = String(input);
package/src/testing.js CHANGED
@@ -124,7 +124,9 @@ class EchoLLM {
124
124
  * there are no documents and no `answer`
125
125
  */
126
126
  constructor({ answer, fallback = 'No information found.' } = {}) {
127
+ /** The fixed answer, or a function of (query, docs) that returns it. */
127
128
  this.answer = answer;
129
+ /** Returned when there are no documents and no `answer`. */
128
130
  this.fallback = fallback;
129
131
  /** @type {Array<{query: string, docs: Array<Object>, options: Object}>} */
130
132
  this.calls = [];
@@ -111,9 +111,13 @@ class BatchProcessor extends EventEmitter {
111
111
  /**
112
112
  * Process items in optimized batches
113
113
  *
114
- * @param {Array<string|Object>} items - Items to process
115
- * @param {Function} processFn - Function to process each batch
116
- * @param {Object} options - Processing options
114
+ * @param {Array<string|Object>} items - Items to process; objects are read from `text` or `content`
115
+ * @param {function(string[], {batchIndex: number, estimatedTokens: number, signal: AbortSignal}): Promise<Array>} processFn - Processes one batch of texts, returning one result per text
116
+ * @param {Object} [options] - Processing options
117
+ * @param {function(Object): void} [options.onProgress]
118
+ * @param {function(Object): void} [options.onBatchComplete]
119
+ * @param {AbortController} [options.abortController] - Abort to stop between batches
120
+ * @param {boolean} [options.returnOriginalOrder=true]
117
121
  * @returns {Promise<Array>} Processed results
118
122
  */
119
123
  async processBatches(items, processFn, options = {}) {
@@ -11,7 +11,9 @@
11
11
  */
12
12
 
13
13
  /**
14
- * Error codes organized by category
14
+ * Known error codes, grouped by prefix (CONFIG_, PLUGIN_, PIPELINE_,
15
+ * VALIDATION_, FS_, NETWORK_, HOT_RELOAD_, DAG_, AUTH_). Each value equals
16
+ * its key.
15
17
  */
16
18
  const ERROR_CODES = {
17
19
  // Configuration errors (CONFIG_*)
@@ -75,7 +77,9 @@ const ERROR_CODES = {
75
77
  };
76
78
 
77
79
  /**
78
- * Error message templates with suggestions
80
+ * Message, suggestion and documentation templates keyed by error code.
81
+ * Placeholders such as {path} are filled from the context passed to
82
+ * formatError. Codes without an entry fall back to UNKNOWN_ERROR.
79
83
  */
80
84
  const ERROR_TEMPLATES = {
81
85
  [ERROR_CODES.CONFIG_NOT_FOUND]: {
@@ -351,9 +355,15 @@ const ERROR_TEMPLATES = {
351
355
  };
352
356
 
353
357
  /**
354
- * Enhanced Error class with code and context
358
+ * Error carrying a code, the context used to build its message, suggested
359
+ * fixes and a documentation path, all taken from ERROR_TEMPLATES.
355
360
  */
356
361
  class EnhancedError extends Error {
362
+ /**
363
+ * @param {string} code - Error code, normally one of ERROR_CODES
364
+ * @param {Object<string, *>} [context={}] - Values for the template placeholders
365
+ * @param {Error|null} [originalError=null] - Underlying error, if any
366
+ */
357
367
  constructor(code, context = {}, originalError = null) {
358
368
  const formattedMessage = formatError(code, context);
359
369
  super(formattedMessage.message);
@@ -373,14 +383,17 @@ class EnhancedError extends Error {
373
383
  }
374
384
 
375
385
  /**
376
- * Get formatted error message with all details
386
+ * Format this error for display with formatErrorMessage's default options.
387
+ * @returns {string} Multi-line message with code, suggestions and documentation link
377
388
  */
378
389
  getFormattedMessage() {
379
390
  return formatErrorMessage(this);
380
391
  }
381
392
 
382
393
  /**
383
- * Convert to JSON for logging
394
+ * Plain object for JSON logging, including the stack and the original
395
+ * error's message and stack (or null).
396
+ * @returns {{name: string, code: string, message: string, suggestions: string[], documentation: string, context: Object<string, *>, timestamp: string, stack: string, originalError: ({message: string, stack: string}|null)}}
384
397
  */
385
398
  toJSON() {
386
399
  return {
@@ -403,11 +416,13 @@ class EnhancedError extends Error {
403
416
  }
404
417
 
405
418
  /**
406
- * Format an error message with context
419
+ * Fill the template for an error code with context values. Unknown codes use
420
+ * the UNKNOWN_ERROR template. Each placeholder is replaced at its first
421
+ * occurrence only; placeholders with no context value stay as written.
407
422
  *
408
423
  * @param {string} code - Error code
409
- * @param {object} context - Context variables
410
- * @returns {object} Formatted error details
424
+ * @param {Object<string, *>} [context={}] - Placeholder values, converted with String()
425
+ * @returns {{code: string, message: string, suggestions: string[], documentation: string, context: Object<string, *>}} Formatted error details
411
426
  */
412
427
  function formatError(code, context = {}) {
413
428
  const template =
@@ -438,10 +453,17 @@ function formatError(code, context = {}) {
438
453
  }
439
454
 
440
455
  /**
441
- * Format error message for display
456
+ * Build a multi-line display string for an error. Fields the error lacks
457
+ * (code, suggestions, documentation) are skipped.
442
458
  *
443
459
  * @param {Error|EnhancedError} error - Error to format
444
- * @param {object} options - Formatting options
460
+ * @param {Object} [options] - Formatting options
461
+ * @param {boolean} [options.includeCode=true] - Prefix the message with the error code
462
+ * @param {boolean} [options.includeSuggestions=true] - List the suggestions
463
+ * @param {boolean} [options.includeDocumentation=true] - Add the documentation URL
464
+ * @param {boolean} [options.includeContext=false] - List the context values as JSON
465
+ * @param {boolean} [options.includeStack=false] - Append the stack trace
466
+ * @param {boolean} [options.color=false] - Accepted but unused
445
467
  * @returns {string} Formatted error message
446
468
  */
447
469
  function formatErrorMessage(error, options = {}) {
@@ -500,11 +522,11 @@ function formatErrorMessage(error, options = {}) {
500
522
  }
501
523
 
502
524
  /**
503
- * Create an enhanced error
525
+ * Create an EnhancedError. Same as calling its constructor.
504
526
  *
505
527
  * @param {string} code - Error code
506
- * @param {object} context - Context variables
507
- * @param {Error} originalError - Original error (optional)
528
+ * @param {Object<string, *>} [context={}] - Placeholder values
529
+ * @param {Error|null} [originalError=null] - Underlying error, if any
508
530
  * @returns {EnhancedError} Enhanced error instance
509
531
  */
510
532
  function createError(code, context = {}, originalError = null) {
@@ -512,11 +534,12 @@ function createError(code, context = {}, originalError = null) {
512
534
  }
513
535
 
514
536
  /**
515
- * Wrap an existing error with enhanced context
537
+ * Wrap an existing error in an EnhancedError. The original message is added
538
+ * to the context as originalMessage and the error is kept as originalError.
516
539
  *
517
540
  * @param {Error} error - Original error
518
541
  * @param {string} code - Error code
519
- * @param {object} context - Additional context
542
+ * @param {Object<string, *>} [context={}] - Additional placeholder values
520
543
  * @returns {EnhancedError} Enhanced error
521
544
  */
522
545
  function wrapError(error, code, context = {}) {
@@ -531,7 +554,7 @@ function wrapError(error, code, context = {}) {
531
554
  }
532
555
 
533
556
  /**
534
- * Check if an error is an EnhancedError
557
+ * Check whether a value is an EnhancedError instance.
535
558
  *
536
559
  * @param {*} error - Value to check
537
560
  * @returns {boolean} True if enhanced error
@@ -541,10 +564,12 @@ function isEnhancedError(error) {
541
564
  }
542
565
 
543
566
  /**
544
- * Get error severity level
567
+ * Severity for an error code: 'critical' for PIPELINE_CREATION_FAILED,
568
+ * CONFIG_NOT_FOUND, FS_PERMISSION_DENIED and AUTH_INVALID_TOKEN, 'warning'
569
+ * for the HOT_RELOAD_FAILED and HOT_RELOAD_STATE_ERROR codes, 'error' otherwise.
545
570
  *
546
571
  * @param {string} code - Error code
547
- * @returns {string} Severity level (critical, error, warning)
572
+ * @returns {'critical'|'warning'|'error'} Severity level
548
573
  */
549
574
  function getErrorSeverity(code) {
550
575
  const critical = [
@@ -565,10 +590,13 @@ function getErrorSeverity(code) {
565
590
  }
566
591
 
567
592
  /**
568
- * Log error with appropriate formatting
593
+ * Format an error and write it to a logger. Warnings go to logger.warn,
594
+ * everything else to logger.error; critical errors include the stack trace.
595
+ * Errors that are not EnhancedError are treated as 'error'.
569
596
  *
570
597
  * @param {Error|EnhancedError} error - Error to log
571
- * @param {object} logger - Logger instance (optional)
598
+ * @param {Pick<Console, 'error'|'warn'>} [logger=console] - Logger to write to
599
+ * @returns {void}
572
600
  */
573
601
  function logError(error, logger = console) {
574
602
  const severity = isEnhancedError(error)
@@ -154,18 +154,38 @@ class StructuredLogger {
154
154
  this.logger[level](logData, message);
155
155
  }
156
156
 
157
+ /**
158
+ * Logs at info level.
159
+ * @param {string} message
160
+ * @param {*} [meta] - An object, Error or value to attach
161
+ */
157
162
  info(message, meta) {
158
163
  this._log('info', message, meta);
159
164
  }
160
165
 
166
+ /**
167
+ * Logs at error level.
168
+ * @param {string} message
169
+ * @param {*} [meta] - An object, Error or value to attach
170
+ */
161
171
  error(message, meta) {
162
172
  this._log('error', message, meta);
163
173
  }
164
174
 
175
+ /**
176
+ * Logs at warn level.
177
+ * @param {string} message
178
+ * @param {*} [meta] - An object, Error or value to attach
179
+ */
165
180
  warn(message, meta) {
166
181
  this._log('warn', message, meta);
167
182
  }
168
183
 
184
+ /**
185
+ * Logs at debug level.
186
+ * @param {string} message
187
+ * @param {*} [meta] - An object, Error or value to attach
188
+ */
169
189
  debug(message, meta) {
170
190
  this._log('debug', message, meta);
171
191
  }
@@ -207,7 +227,7 @@ class StructuredLogger {
207
227
  }
208
228
  }
209
229
 
210
- // Create default logger instance
230
+ /** The shared structured logger (pino), exported as `logger.logger`. */
211
231
  const logger = new StructuredLogger(baseLogger);
212
232
 
213
233
  /**
@@ -238,4 +258,14 @@ module.exports = {
238
258
  createLogger,
239
259
  correlationMiddleware,
240
260
  StructuredLogger,
261
+ // The package exports this module as `logger`, so `logger.info()` and the
262
+ // rest log through the shared instance as well as `logger.logger.info()`.
263
+ /** Logs at info level. Same as `logger.logger.info()`. @param {string} message @param {*} [meta] */
264
+ info: (message, meta) => logger.info(message, meta),
265
+ /** Logs at warn level. Same as `logger.logger.warn()`. @param {string} message @param {*} [meta] */
266
+ warn: (message, meta) => logger.warn(message, meta),
267
+ /** Logs at error level. Same as `logger.logger.error()`. @param {string} message @param {*} [meta] */
268
+ error: (message, meta) => logger.error(message, meta),
269
+ /** Logs at debug level. Same as `logger.logger.debug()`. @param {string} message @param {*} [meta] */
270
+ debug: (message, meta) => logger.debug(message, meta),
241
271
  };
@@ -11,14 +11,19 @@
11
11
 
12
12
  const crypto = require('crypto');
13
13
 
14
+ /**
15
+ * Sliding-window limiter: allows up to `maxAttempts` requests per identifier
16
+ * in `windowMs`, then blocks that identifier for `blockDurationMs`. Check a
17
+ * request with allowRequest(identifier).
18
+ */
14
19
  class RateLimiter {
15
20
  /**
16
21
  * Create a rate limiter instance
17
22
  * @param {Object} options - Configuration options
18
- * @param {number} options.maxAttempts - Maximum attempts allowed (default: 5)
19
- * @param {number} options.windowMs - Time window in milliseconds (default: 15 minutes)
20
- * @param {number} options.blockDurationMs - How long to block after exceeding limit (default: 1 hour)
21
- * @param {number} options.cleanupIntervalMs - How often to cleanup expired entries (default: 5 minutes)
23
+ * @param {number} [options.maxAttempts=5] - Maximum attempts allowed (default: 5)
24
+ * @param {number} [options.windowMs=900000] - Time window in milliseconds (default: 15 minutes)
25
+ * @param {number} [options.blockDurationMs=3600000] - How long to block after exceeding limit (default: 1 hour)
26
+ * @param {number} [options.cleanupIntervalMs=300000] - How often to cleanup expired entries (default: 5 minutes)
22
27
  */
23
28
  constructor(options = {}) {
24
29
  this.maxAttempts = options.maxAttempts || 5;
@@ -75,7 +75,16 @@ async function retry(fn, opts = {}) {
75
75
  }
76
76
  }
77
77
 
78
- // Legacy wrapper for backward compatibility
78
+ /**
79
+ * Older wrapper around retry(): retries `fn` with exponential backoff from
80
+ * `initialDelay` ms and logs each retry and the final failure under `label`.
81
+ * @param {function(number): *} fn - The operation; called with the attempt index
82
+ * @param {Object} [options]
83
+ * @param {number} [options.retries=3] - Retries after the first attempt
84
+ * @param {number} [options.initialDelay=300] - First backoff delay in ms
85
+ * @param {string} [options.label='operation'] - Name used in log messages
86
+ * @returns {Promise<*>} The operation's result
87
+ */
79
88
  async function withRetry(
80
89
  _fn,
81
90
  { retries = 3, initialDelay = 300, label = 'operation' } = {},