@devilsdev/rag-pipeline-utils 2.4.5 → 2.6.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 (258) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +431 -0
  3. package/README.md +418 -117
  4. package/SECURITY.md +5 -5
  5. package/dist/experimental.cjs +2 -0
  6. package/dist/experimental.d.ts +1 -0
  7. package/dist/experimental.mjs +5 -0
  8. package/dist/index.cjs +6442 -1355
  9. package/dist/index.d.ts +129 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/testing.cjs +2 -0
  12. package/dist/testing.d.ts +1 -0
  13. package/dist/testing.mjs +5 -0
  14. package/dist/types/adapters/ai-sdk.d.ts +53 -0
  15. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  16. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  17. package/dist/types/agentic/query-planner.d.ts +145 -0
  18. package/dist/types/agentic/self-critique.d.ts +45 -0
  19. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  20. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  21. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  22. package/dist/types/ai/multimodal.d.ts +39 -0
  23. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  24. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  25. package/dist/types/cache/component-keys.d.ts +7 -0
  26. package/dist/types/cache/memory-cache.d.ts +54 -0
  27. package/dist/types/cache/pipeline-cache.d.ts +66 -0
  28. package/dist/types/cache/redis-cache.d.ts +46 -0
  29. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  30. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  31. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  32. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  33. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  34. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  35. package/dist/types/citation/citation-tracker.d.ts +64 -0
  36. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  37. package/dist/types/citation/source-mapper.d.ts +37 -0
  38. package/dist/types/connectors/anthropic-connector.d.ts +97 -0
  39. package/dist/types/connectors/base-connector.d.ts +87 -0
  40. package/dist/types/connectors/cohere-connector.d.ts +94 -0
  41. package/dist/types/connectors/connector-utils.d.ts +110 -0
  42. package/dist/types/connectors/local-embedder.d.ts +95 -0
  43. package/dist/types/connectors/memory-retriever.d.ts +99 -0
  44. package/dist/types/connectors/ollama-connector.d.ts +115 -0
  45. package/dist/types/connectors/openai-connector.d.ts +129 -0
  46. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  47. package/dist/types/core/create-pipeline.d.ts +8 -0
  48. package/dist/types/core/plugin-registry.d.ts +136 -0
  49. package/dist/types/core/stage-contracts.d.ts +36 -0
  50. package/dist/types/core/validate-plugin.d.ts +45 -0
  51. package/dist/types/cost/cost-calculator.d.ts +152 -0
  52. package/dist/types/cost/cost-tracker.d.ts +126 -0
  53. package/dist/types/cost/token-budget.d.ts +110 -0
  54. package/dist/types/dag/core/error-context.d.ts +239 -0
  55. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  56. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  57. package/dist/types/dag/dag-engine.d.ts +156 -0
  58. package/dist/types/dag/dag-node.d.ts +37 -0
  59. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  60. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  61. package/dist/types/debug/execution-tracer.d.ts +57 -0
  62. package/dist/types/debug/trace-inspector.d.ts +31 -0
  63. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  64. package/dist/types/enterprise/data-governance.d.ts +203 -0
  65. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  66. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  67. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  68. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  69. package/dist/types/evaluate/groundedness.d.ts +60 -0
  70. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  71. package/dist/types/evaluate/relevance.d.ts +21 -0
  72. package/dist/types/evaluate/scoring.d.ts +123 -0
  73. package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
  74. package/dist/types/experimental.d.ts +15 -0
  75. package/dist/types/graph/entity-extractor.d.ts +84 -0
  76. package/dist/types/graph/graph-index.d.ts +49 -0
  77. package/dist/types/graph/graph-retriever.d.ts +81 -0
  78. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  79. package/dist/types/grounding/check-answer.d.ts +101 -0
  80. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  81. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  82. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  83. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  84. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  85. package/dist/types/mcp/mcp-server.d.ts +85 -0
  86. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  87. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  88. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  89. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  90. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  91. package/dist/types/retrieval/file-vector-store.d.ts +99 -0
  92. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  93. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  94. package/dist/types/security/jwt-config.d.ts +80 -0
  95. package/dist/types/security/jwt-validator.d.ts +195 -0
  96. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  97. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  98. package/dist/types/security/validator.d.ts +5 -0
  99. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  100. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  101. package/dist/types/streaming/stream-router.d.ts +88 -0
  102. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  103. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  104. package/dist/types/templates/scaffold.d.ts +55 -0
  105. package/dist/types/templates/template-registry.d.ts +63 -0
  106. package/dist/types/testing.d.ts +168 -0
  107. package/dist/types/utils/embed-query.d.ts +20 -0
  108. package/dist/types/utils/logger.d.ts +50 -0
  109. package/dist/types/utils/rate-limiter.d.ts +66 -0
  110. package/dist/types/utils/retry.d.ts +27 -0
  111. package/dist/types/utils/run-abort.d.ts +34 -0
  112. package/dist/types/utils/secure-logging.d.ts +51 -0
  113. package/dist/types/utils/structured-logger.d.ts +126 -0
  114. package/package.json +72 -33
  115. package/src/adapters/ai-sdk.js +215 -0
  116. package/src/agentic/agentic-pipeline.js +75 -7
  117. package/src/agentic/iterative-retriever.js +47 -10
  118. package/src/agentic/query-planner.js +329 -78
  119. package/src/cache/component-keys.js +38 -0
  120. package/src/cache/memory-cache.js +87 -0
  121. package/src/cache/pipeline-cache.js +260 -0
  122. package/src/cache/redis-cache.js +83 -0
  123. package/src/citation/citation-tracker.js +6 -4
  124. package/src/citation/hallucination-detector.js +2 -2
  125. package/src/citation/source-mapper.js +15 -28
  126. package/src/cli/commands/ai.js +106 -0
  127. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  128. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  129. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  130. package/src/cli/commands/plugin-hub.js +114 -49
  131. package/src/cli/diagnostics/config-checker.js +76 -63
  132. package/src/cli/diagnostics/environment-checker.js +62 -31
  133. package/src/cli/diagnostics/plugin-checker.js +36 -89
  134. package/src/cli/doctor-command.js +39 -5
  135. package/src/cli/enhanced-cli-commands.js +104 -233
  136. package/src/cli/handlers/ai-handler.js +364 -0
  137. package/src/cli/handlers/diagnostics-handler.js +78 -31
  138. package/src/cli/handlers/ingest-handler.js +28 -22
  139. package/src/cli/handlers/init-handler.js +148 -25
  140. package/src/cli/handlers/query-handler.js +203 -25
  141. package/src/cli/handlers/run-handler.js +16 -167
  142. package/src/cli/handlers/serve-handler.js +60 -0
  143. package/src/cli/services/bench-stats.js +54 -0
  144. package/src/cli/services/benchmark.js +161 -0
  145. package/src/cli/services/feedback.js +155 -0
  146. package/src/cli/services/instrument.js +83 -0
  147. package/src/cli/services/rag-service.js +361 -0
  148. package/src/cli/shared/cli-utils.js +24 -7
  149. package/src/cli/shared/config-context.js +21 -0
  150. package/src/cli/wizard/config-builder.js +81 -0
  151. package/src/cli/wizard/init-wizard.js +602 -0
  152. package/src/config/component-factory.js +125 -0
  153. package/src/config/enhanced-ragrc-schema.js +0 -2
  154. package/src/config/load-config.js +29 -10
  155. package/src/config/load-plugin-config.js +46 -2
  156. package/src/config/validate-schema.js +4 -1
  157. package/src/connectors/anthropic-connector.js +115 -57
  158. package/src/connectors/base-connector.js +74 -19
  159. package/src/connectors/cohere-connector.js +108 -34
  160. package/src/connectors/connector-utils.js +302 -0
  161. package/src/connectors/local-embedder.js +63 -5
  162. package/src/connectors/memory-retriever.js +125 -14
  163. package/src/connectors/ollama-connector.js +112 -51
  164. package/src/connectors/openai-connector.js +153 -44
  165. package/src/connectors/rag-prompt.js +88 -0
  166. package/src/core/create-pipeline.js +307 -61
  167. package/src/core/performance/parallel-processor.js +62 -1
  168. package/src/core/plugin-registry.js +125 -26
  169. package/src/core/stage-contracts.js +36 -0
  170. package/src/core/validate-plugin.js +85 -0
  171. package/src/cost/cost-calculator.js +205 -32
  172. package/src/debug/execution-tracer.js +14 -4
  173. package/src/dev/dev-server.js +173 -11
  174. package/src/dev/hot-reload.js +44 -4
  175. package/src/ecosystem/plugin-checks.js +181 -0
  176. package/src/ecosystem/plugin-hub.js +50 -125
  177. package/src/ecosystem/plugin-installer.js +298 -0
  178. package/src/ecosystem/plugin-probe.js +127 -0
  179. package/src/ecosystem/plugin-scanner.js +185 -0
  180. package/src/ecosystem/tar-extract.js +114 -0
  181. package/src/enterprise/audit-logging.js +158 -29
  182. package/src/enterprise/sso-integration.js +716 -228
  183. package/src/evaluate/context-metrics.js +28 -16
  184. package/src/evaluate/evaluator.js +3 -3
  185. package/src/evaluate/faithfulness.js +18 -14
  186. package/src/evaluate/groundedness.js +43 -12
  187. package/src/evaluate/pipeline-evaluator.js +35 -8
  188. package/src/evaluate/scoring.js +251 -3
  189. package/src/evaluate/semantic-similarity.js +79 -0
  190. package/src/experimental.js +71 -0
  191. package/src/grounding/check-answer.js +173 -0
  192. package/src/guardrails/guardrails-pipeline.js +69 -7
  193. package/src/guardrails/injection-detector.js +283 -0
  194. package/src/guardrails/post-generation-guard.js +19 -0
  195. package/src/guardrails/pre-retrieval-guard.js +36 -36
  196. package/src/guardrails/retrieval-guard.js +58 -8
  197. package/src/index.js +58 -1
  198. package/src/loader/csv-loader.js +3 -2
  199. package/src/loader/document-reader.js +91 -0
  200. package/src/loader/html-loader.js +3 -1
  201. package/src/loader/markdown-loader.js +3 -2
  202. package/src/reranker/cascade-reranker.js +3 -0
  203. package/src/reranker/embedding-reranker.js +136 -21
  204. package/src/reranker/llm-reranker.js +79 -12
  205. package/src/reranker/scoring-reranker.js +3 -0
  206. package/src/retrieval/bm25-search.js +12 -6
  207. package/src/retrieval/file-vector-store.js +241 -0
  208. package/src/retrieval/hybrid-retriever.js +22 -19
  209. package/src/security/jwt-config.js +13 -2
  210. package/src/security/jwt-validator.js +139 -34
  211. package/src/security/plugin-sandbox.js +18 -1
  212. package/src/security/plugin-signature-verifier.js +245 -70
  213. package/src/testing.js +254 -0
  214. package/src/utils/deprecate.js +69 -0
  215. package/src/utils/embed-query.js +33 -0
  216. package/src/utils/input-sanitizer.js +19 -4
  217. package/src/utils/logger.js +34 -4
  218. package/src/utils/run-abort.js +102 -0
  219. package/src/ai/federated-learning.js +0 -211
  220. package/src/ai/federation.js +0 -71
  221. package/src/ai/index.js +0 -22
  222. package/src/ai/model-trainer.js +0 -217
  223. package/src/ai/model-training.js +0 -202
  224. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  225. package/src/ai/orchestrator.js +0 -65
  226. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  227. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  228. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  229. package/src/cli/commands/ai-ml/index.js +0 -17
  230. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  231. package/src/cli/commands/ai-ml/shared.js +0 -51
  232. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  233. package/src/cli/commands/ai-ml.js +0 -386
  234. package/src/cli/commands/docs.js +0 -228
  235. package/src/cli/commands/dx.js +0 -538
  236. package/src/cli/commands/wizard.js +0 -627
  237. package/src/cli/input-sanitization-middleware.js +0 -356
  238. package/src/cli/interactive-wizard.js +0 -373
  239. package/src/cli/templates/development.template.json +0 -64
  240. package/src/cli/templates/minimal.template.json +0 -33
  241. package/src/cli/templates/production.template.json +0 -104
  242. package/src/cli/templates/testing.template.json +0 -45
  243. package/src/cli/wizard/configuration.js +0 -148
  244. package/src/cli/wizard/observability.js +0 -79
  245. package/src/cli/wizard/performance.js +0 -76
  246. package/src/cli/wizard/plugin-selection.js +0 -297
  247. package/src/cli/wizard/preview-save.js +0 -172
  248. package/src/cli/wizard/project-setup.js +0 -66
  249. package/src/core/pipeline-factory.js +0 -59
  250. package/src/dag/enhanced-dag-engine.js +0 -894
  251. package/src/dx/index.js +0 -40
  252. package/src/dx/integration-templates.js +0 -662
  253. package/src/dx/performance-profiler.js +0 -968
  254. package/src/dx/realtime-debugger.js +0 -755
  255. package/src/dx/visual-pipeline-builder.js +0 -578
  256. package/src/ingest.js +0 -59
  257. package/src/pipeline/pipeline.js +0 -18
  258. package/src/query.js +0 -117
@@ -28,10 +28,17 @@ function generateTraceId() {
28
28
  }
29
29
 
30
30
  /**
31
- * Truncate a value if its JSON representation exceeds maxSize bytes
32
- * @param {*} value - Value to truncate
31
+ * Snapshot a value for a trace: a JSON copy, or a truncated string when its
32
+ * JSON exceeds maxSize characters.
33
+ *
34
+ * Always a copy, never the live object: the traced run attaches the trace to
35
+ * its result (`result.trace`), so storing the result itself made the trace
36
+ * circular and exportTraces('json') threw. A copy also keeps later changes to
37
+ * the result out of the recorded trace.
38
+ *
39
+ * @param {*} value - Value to snapshot
33
40
  * @param {number} maxSize - Maximum serialized size in characters
34
- * @returns {*} Original value or truncated string
41
+ * @returns {*} JSON copy of the value, or a truncated string
35
42
  */
36
43
  function truncatePayload(value, maxSize) {
37
44
  if (value === undefined || value === null) {
@@ -43,8 +50,11 @@ function truncatePayload(value, maxSize) {
43
50
  } catch {
44
51
  return '[unserializable]';
45
52
  }
53
+ if (serialized === undefined) {
54
+ return undefined; // functions and symbols have no JSON form
55
+ }
46
56
  if (serialized.length <= maxSize) {
47
- return value;
57
+ return JSON.parse(serialized);
48
58
  }
49
59
  return serialized.slice(0, maxSize) + '...[truncated]';
50
60
  }
@@ -7,20 +7,25 @@
7
7
  * for rapid plugin and configuration development.
8
8
  *
9
9
  * Features:
10
+ * - Local HTTP API over the configured pipeline:
11
+ * GET /health -> { status, uptime }
12
+ * POST /query -> { question, topK? } => { answer, results, citations }
13
+ * Binds to 127.0.0.1 by default, JSON only, no CORS headers, request
14
+ * bodies limited to 64 KB, no static file serving.
10
15
  * - Automatic hot reloading of plugins and configuration
11
- * - Live feedback on reload status
12
- * - Error notifications
13
16
  * - Statistics dashboard
14
- * - WebSocket support for real-time updates (optional)
15
17
  *
16
18
  * @module dev/dev-server
17
19
  * @since 2.4.0
18
20
  */
19
21
 
22
+ const http = require('http');
20
23
  const { EventEmitter } = require('events');
21
24
  const { createHotReloadManager } = require('./hot-reload');
22
25
  const { logger } = require('../utils/logger');
23
26
 
27
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
28
+
24
29
  /**
25
30
  * Development Server
26
31
  *
@@ -31,9 +36,12 @@ class DevServer extends EventEmitter {
31
36
  super();
32
37
 
33
38
  this.options = {
34
- port: options.port || 3000,
35
- host: options.host || 'localhost',
36
- watchPaths: options.watchPaths || ['./src/plugins', './src/mocks'],
39
+ port: options.port ?? 3000, // 0 picks a free port
40
+ host: options.host || '127.0.0.1',
41
+ watchPaths: options.watchPaths || [],
42
+ maxBodyBytes: options.maxBodyBytes || 64 * 1024,
43
+ // Optional custom handler: async ({ question, topK }) => result
44
+ query: options.query || null,
37
45
  configPath: options.configPath || '.ragrc.json',
38
46
  verbose: options.verbose !== false,
39
47
  autoReload: options.autoReload !== false,
@@ -147,12 +155,23 @@ class DevServer extends EventEmitter {
147
155
  // Start hot reload
148
156
  await this.hotReload.start();
149
157
 
158
+ this.server = http.createServer((req, res) => {
159
+ this._handleRequest(req, res).catch((error) => {
160
+ this._send(res, 500, { error: error.message });
161
+ });
162
+ });
163
+ await new Promise((resolve, reject) => {
164
+ this.server.once('error', reject);
165
+ this.server.listen(this.options.port, this.options.host, () => {
166
+ this.server.off('error', reject);
167
+ resolve();
168
+ });
169
+ });
170
+ this.port = this.server.address().port;
171
+
150
172
  this.isRunning = true;
151
173
 
152
- this._log(
153
- `Development server started on ${this.options.host}:${this.options.port}`,
154
- 'success',
155
- );
174
+ this._log(`Development server listening on ${this.url}`, 'success');
156
175
  this._log(`Watching: ${this.options.watchPaths.join(', ')}`, 'info');
157
176
  this._log(
158
177
  'Hot reload enabled - changes will be applied automatically',
@@ -178,6 +197,11 @@ class DevServer extends EventEmitter {
178
197
  // Stop hot reload
179
198
  await this.hotReload.stop();
180
199
 
200
+ if (this.server) {
201
+ await new Promise((resolve) => this.server.close(() => resolve()));
202
+ this.server = null;
203
+ }
204
+
181
205
  this.isRunning = false;
182
206
 
183
207
  const uptime = this.startTime ? Date.now() - this.startTime : 0;
@@ -202,7 +226,8 @@ class DevServer extends EventEmitter {
202
226
  isRunning: this.isRunning,
203
227
  uptime,
204
228
  host: this.options.host,
205
- port: this.options.port,
229
+ port: this.port ?? this.options.port,
230
+ url: this.isRunning ? this.url : null,
206
231
  watchPaths: this.options.watchPaths,
207
232
  configPath: this.options.configPath,
208
233
  hotReload: {
@@ -249,6 +274,143 @@ ${this.options.watchPaths.map((p) => `║ - ${p.padEnd(56)}║`).join('\n')}
249
274
  * @param {string} type - Type ('plugin' or 'config')
250
275
  * @returns {Promise<void>}
251
276
  */
277
+ /** @returns {string} Base URL of the running server */
278
+ get url() {
279
+ return `http://${this.options.host}:${this.port ?? this.options.port}`;
280
+ }
281
+
282
+ /**
283
+ * Route a request. Only GET /health and POST /query exist.
284
+ * @private
285
+ */
286
+ async _handleRequest(req, res) {
287
+ // DNS rebinding: a web page can point its own domain at 127.0.0.1 and
288
+ // then call this server same-origin. When bound to loopback, only accept
289
+ // requests addressed to a loopback name.
290
+ if (
291
+ LOOPBACK_HOSTS.has(this.options.host) &&
292
+ !this._isLoopbackHostHeader(req)
293
+ ) {
294
+ return this._send(res, 403, { error: 'Forbidden host' });
295
+ }
296
+
297
+ const { pathname } = new URL(req.url, 'http://localhost');
298
+
299
+ if (pathname === '/health') {
300
+ if (req.method !== 'GET')
301
+ return this._send(res, 405, { error: 'Use GET' });
302
+ const uptime = this.startTime ? Date.now() - this.startTime : 0;
303
+ return this._send(res, 200, { status: 'ok', uptime });
304
+ }
305
+
306
+ if (pathname === '/query') {
307
+ if (req.method !== 'POST')
308
+ return this._send(res, 405, { error: 'Use POST' });
309
+ let body;
310
+ try {
311
+ body = await this._readJson(req);
312
+ } catch (error) {
313
+ return this._send(res, error.statusCode || 400, {
314
+ error: error.message,
315
+ });
316
+ }
317
+ const { question, topK } = body || {};
318
+ if (typeof question !== 'string' || !question.trim()) {
319
+ return this._send(res, 400, {
320
+ error: '"question" must be a non-empty string',
321
+ });
322
+ }
323
+ if (
324
+ topK !== undefined &&
325
+ !(Number.isInteger(topK) && topK > 0 && topK <= 50)
326
+ ) {
327
+ return this._send(res, 400, {
328
+ error: '"topK" must be an integer from 1 to 50',
329
+ });
330
+ }
331
+ const result = await this._runQuery({ question, topK: topK || 3 });
332
+ return this._send(res, 200, result);
333
+ }
334
+
335
+ return this._send(res, 404, { error: 'Not found' });
336
+ }
337
+
338
+ /** @private */
339
+ _isLoopbackHostHeader(req) {
340
+ const header = req.headers.host || '';
341
+ const hostname = header.startsWith('[')
342
+ ? header.slice(0, header.indexOf(']') + 1)
343
+ : header.split(':')[0];
344
+ return LOOPBACK_HOSTS.has(hostname.toLowerCase());
345
+ }
346
+
347
+ /**
348
+ * Answer with the custom `query` option, or the pipeline described by the
349
+ * config file (reloaded per request, so config edits apply immediately).
350
+ * @private
351
+ */
352
+ async _runQuery({ question, topK }) {
353
+ if (this.options.query) return this.options.query({ question, topK });
354
+ const { loadCliConfig } = require('../cli/shared/config-context');
355
+ const { query } = require('../cli/services/rag-service');
356
+ const { config, configDir } = loadCliConfig({
357
+ config: this.options.configPath,
358
+ });
359
+ // The HTTP response keeps its shape: answer, results, citations.
360
+ const { timings, durationMs, ...result } = await query({
361
+ config,
362
+ configDir,
363
+ question,
364
+ topK,
365
+ });
366
+ return result;
367
+ }
368
+
369
+ /** @private */
370
+ _readJson(req) {
371
+ return new Promise((resolve, reject) => {
372
+ const type = req.headers['content-type'] || '';
373
+ if (!type.includes('application/json')) {
374
+ const error = new Error('Content-Type must be application/json');
375
+ error.statusCode = 415;
376
+ return reject(error);
377
+ }
378
+ let size = 0;
379
+ const chunks = [];
380
+ req.on('data', (chunk) => {
381
+ size += chunk.length;
382
+ if (size > this.options.maxBodyBytes) {
383
+ const error = new Error('Request body too large');
384
+ error.statusCode = 413;
385
+ reject(error);
386
+ // Stop buffering but keep draining, so the 413 response is delivered
387
+ req.removeAllListeners('data');
388
+ req.resume();
389
+ return;
390
+ }
391
+ chunks.push(chunk);
392
+ });
393
+ req.on('end', () => {
394
+ try {
395
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
396
+ } catch {
397
+ reject(new Error('Invalid JSON body'));
398
+ }
399
+ });
400
+ req.on('error', reject);
401
+ });
402
+ }
403
+
404
+ /** @private */
405
+ _send(res, status, payload) {
406
+ if (res.headersSent) return;
407
+ res.writeHead(status, {
408
+ 'Content-Type': 'application/json; charset=utf-8',
409
+ 'X-Content-Type-Options': 'nosniff',
410
+ });
411
+ res.end(JSON.stringify(payload));
412
+ }
413
+
252
414
  async triggerReload(filePath, type = 'plugin') {
253
415
  this._log(`Manually triggering reload: ${filePath}`, 'info');
254
416
  await this.hotReload.triggerReload(filePath, type);
@@ -18,7 +18,8 @@
18
18
  */
19
19
 
20
20
  const { EventEmitter } = require('events');
21
- const chokidar = require('chokidar');
21
+ // chokidar loads when watching starts, not on import.
22
+ const loadChokidar = () => require('chokidar'); // eslint-disable-line global-require
22
23
  const path = require('path');
23
24
  const fs = require('fs');
24
25
  const {
@@ -28,6 +29,42 @@ const {
28
29
  } = require('../utils/error-formatter');
29
30
  const { logger } = require('../utils/logger');
30
31
 
32
+ const RELOADABLE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']);
33
+
34
+ /**
35
+ * chokidar 4 removed glob support, so glob strings in `ignored` were treated
36
+ * as literal paths and matched nothing. Convert them to one matcher function.
37
+ *
38
+ * @param {Array<string|RegExp|Function>} patterns
39
+ * @returns {(filePath: string) => boolean}
40
+ */
41
+ function toIgnoreMatcher(patterns) {
42
+ const matchers = patterns.map((pattern) => {
43
+ if (typeof pattern === 'function') return pattern;
44
+ if (pattern instanceof RegExp) return (p) => pattern.test(p);
45
+ const regex = new RegExp(
46
+ '^' +
47
+ pattern
48
+ .split('**/')
49
+ .map((part) =>
50
+ part
51
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
52
+ .replace(/\*\*/g, '\u0000') // a remaining ** (e.g. trailing) spans directories
53
+ .replace(/\*/g, '[^/]*')
54
+ .replace(/\?/g, '[^/]')
55
+ .replace(/\u0000/g, '.*'),
56
+ )
57
+ .join('(?:.*/)?') +
58
+ '$',
59
+ );
60
+ return (p) => regex.test(p);
61
+ });
62
+ return (filePath) => {
63
+ const normalized = filePath.split(path.sep).join('/');
64
+ return matchers.some((match) => match(normalized));
65
+ };
66
+ }
67
+
31
68
  /**
32
69
  * Hot Reload Manager
33
70
  *
@@ -134,8 +171,8 @@ class HotReloadManager extends EventEmitter {
134
171
  * @returns {Promise<void>}
135
172
  */
136
173
  async _watchPlugins() {
137
- const watcher = chokidar.watch(this.options.watchPaths, {
138
- ignored: this.options.ignorePatterns,
174
+ const watcher = loadChokidar().watch(this.options.watchPaths, {
175
+ ignored: toIgnoreMatcher(this.options.ignorePatterns),
139
176
  persistent: true,
140
177
  ignoreInitial: true,
141
178
  awaitWriteFinish: {
@@ -178,7 +215,7 @@ class HotReloadManager extends EventEmitter {
178
215
  return;
179
216
  }
180
217
 
181
- const watcher = chokidar.watch(this.options.configPath, {
218
+ const watcher = loadChokidar().watch(this.options.configPath, {
182
219
  persistent: true,
183
220
  ignoreInitial: true,
184
221
  awaitWriteFinish: {
@@ -206,6 +243,8 @@ class HotReloadManager extends EventEmitter {
206
243
  * @param {string} filePath - Changed file path
207
244
  */
208
245
  _handlePluginChange(filePath) {
246
+ // Only JavaScript modules can be re-required; ignore data and docs.
247
+ if (!RELOADABLE_EXTENSIONS.has(path.extname(filePath))) return;
209
248
  this._log(`Plugin changed: ${filePath}`);
210
249
 
211
250
  // Debounce rapid changes
@@ -803,6 +842,7 @@ function createHotReloadManager(options = {}) {
803
842
  }
804
843
 
805
844
  module.exports = {
845
+ toIgnoreMatcher,
806
846
  HotReloadManager,
807
847
  createHotReloadManager,
808
848
  };
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Automated certification checks for a local plugin. Every score is computed
5
+ * from the plugin's files: the stage contract (checked in a separate
6
+ * process), the static security scan, tests and documentation. Nothing is
7
+ * assumed; checks that cannot be computed score 0 with a reason.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { scanPlugin } = require('./plugin-scanner');
13
+ const { probeContract } = require('./plugin-probe');
14
+ const { relativeWithin } = require('./tar-extract');
15
+
16
+ /** Automated score needed for each level (manual review/audit are separate). */
17
+ const LEVELS = [
18
+ {
19
+ level: 'ENTERPRISE',
20
+ score: 95,
21
+ requires: 'manual review and security audit',
22
+ },
23
+ { level: 'VERIFIED', score: 80, requires: 'manual review' },
24
+ { level: 'BASIC', score: 60, requires: null },
25
+ ];
26
+
27
+ const TEST_FILE = /(^|[\\/])(__tests__|tests?)[\\/]|\.(test|spec)\.[cm]?js$/;
28
+
29
+ function listFiles(dir) {
30
+ const out = [];
31
+ const walk = (current) => {
32
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
33
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
34
+ const full = path.join(current, entry.name);
35
+ if (entry.isDirectory()) walk(full);
36
+ else if (entry.isFile()) out.push(relativeWithin(dir, full));
37
+ }
38
+ };
39
+ walk(dir);
40
+ return out;
41
+ }
42
+
43
+ function securityCheck(root) {
44
+ const scan = scanPlugin(root);
45
+ const { high, medium, low } = scan.counts;
46
+ const score = Math.max(0, 100 - 40 * high - 10 * medium - 2 * low);
47
+ return {
48
+ id: 'security',
49
+ label: 'Security scan',
50
+ weight: 30,
51
+ score,
52
+ details: scan.findings.map(
53
+ (f) =>
54
+ `${f.severity}: ${f.message} (${f.file}${f.line ? `:${f.line}` : ''})`,
55
+ ),
56
+ };
57
+ }
58
+
59
+ function testsCheck(files, manifest) {
60
+ const testFiles = files.filter((f) => TEST_FILE.test(f));
61
+ const script = manifest.scripts?.test;
62
+ const realScript = Boolean(script) && !/no test specified/.test(script);
63
+ let score = 0;
64
+ if (testFiles.length > 0) score = realScript ? 100 : 60;
65
+ return {
66
+ id: 'tests',
67
+ label: 'Tests',
68
+ weight: 15,
69
+ score,
70
+ details: [
71
+ `${testFiles.length} test file(s)`,
72
+ realScript
73
+ ? `test script: ${script}`
74
+ : 'no "test" script in package.json',
75
+ ],
76
+ };
77
+ }
78
+
79
+ function docsCheck(root, files, manifest) {
80
+ const readme = files.find((f) => /^readme(\.md|\.txt)?$/i.test(f));
81
+ const readmeLength = readme
82
+ ? fs.readFileSync(path.join(root, readme), 'utf8').trim().length
83
+ : 0;
84
+ const parts = [
85
+ [Boolean(readme), 50, readme ? `${readme} present` : 'no README'],
86
+ [
87
+ readmeLength >= 300,
88
+ 20,
89
+ `README length ${readmeLength} chars (300+ expected)`,
90
+ ],
91
+ [
92
+ Boolean(manifest.description),
93
+ 15,
94
+ manifest.description
95
+ ? 'package description present'
96
+ : 'no package description',
97
+ ],
98
+ [
99
+ Boolean(manifest.license),
100
+ 15,
101
+ manifest.license ? `license: ${manifest.license}` : 'no license field',
102
+ ],
103
+ ];
104
+ return {
105
+ id: 'documentation',
106
+ label: 'Documentation',
107
+ weight: 20,
108
+ score: parts.reduce((sum, [ok, points]) => sum + (ok ? points : 0), 0),
109
+ details: parts.map(([ok, , text]) => `${ok ? 'ok' : 'missing'}: ${text}`),
110
+ };
111
+ }
112
+
113
+ /**
114
+ * @param {string} pluginPath - Plugin directory or single .js file
115
+ * @param {object} options
116
+ * @param {string} [options.stage] - Overrides package.json "ragPipeline.stage"
117
+ * @returns {Promise<{plugin: string, stage: string, checks: object[], score: number, level: string|null, nextLevel: object|null}>}
118
+ */
119
+ async function runPluginChecks(pluginPath, options = {}) {
120
+ const absolute = path.resolve(pluginPath);
121
+ if (!fs.existsSync(absolute))
122
+ throw new Error(`Plugin not found: ${pluginPath}`);
123
+ const isDir = fs.statSync(absolute).isDirectory();
124
+ const root = isDir ? absolute : path.dirname(absolute);
125
+ const manifestPath = path.join(root, 'package.json');
126
+ const manifest =
127
+ isDir && fs.existsSync(manifestPath)
128
+ ? JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
129
+ : {};
130
+ const stage = options.stage || manifest.ragPipeline?.stage;
131
+ if (!stage) {
132
+ throw new Error(
133
+ 'Unknown stage. Pass --stage or set "ragPipeline.stage" in package.json.',
134
+ );
135
+ }
136
+ const entry = isDir
137
+ ? path.resolve(root, manifest.main || 'index.js')
138
+ : absolute;
139
+ const files = isDir ? listFiles(root) : [path.basename(absolute)];
140
+
141
+ const probe = await probeContract(entry, stage);
142
+ const contract = {
143
+ id: 'contract',
144
+ label: `${stage} contract`,
145
+ weight: 35,
146
+ score: probe.ok ? 100 : 0,
147
+ details: probe.ok
148
+ ? [`methods: ${probe.methods.join(', ')}`]
149
+ : [probe.error || `missing: ${probe.missing.join(', ')}`],
150
+ };
151
+
152
+ const checks = [
153
+ contract,
154
+ isDir ? securityCheck(root) : securityCheck(absolute),
155
+ testsCheck(files, manifest),
156
+ docsCheck(root, isDir ? files : [], manifest),
157
+ ];
158
+ const totalWeight = checks.reduce((sum, c) => sum + c.weight, 0);
159
+ const score = Math.round(
160
+ checks.reduce((sum, c) => sum + c.score * c.weight, 0) / totalWeight,
161
+ );
162
+
163
+ // A failed contract means the plugin cannot run, whatever else scores.
164
+ const eligible =
165
+ contract.score === 100 ? LEVELS.filter((l) => score >= l.score) : [];
166
+ const best = eligible[0] || null;
167
+ const nextLevel = [...LEVELS].reverse().find((l) => score < l.score) || null;
168
+
169
+ return {
170
+ plugin: manifest.name || path.basename(absolute),
171
+ version: manifest.version || null,
172
+ stage,
173
+ checks,
174
+ score,
175
+ level: best ? best.level : null,
176
+ pendingReview: best ? best.requires : null,
177
+ nextLevel: nextLevel && { level: nextLevel.level, score: nextLevel.score },
178
+ };
179
+ }
180
+
181
+ module.exports = { runPluginChecks, LEVELS };