@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
@@ -2,12 +2,28 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const Ajv = require('ajv');
5
+ // ajv loads with the contracts; see _ensureContracts().
6
+ const loadAjv = () => require('ajv'); // eslint-disable-line global-require
6
7
  const {
7
8
  createPluginVerifier,
8
9
  } = require('../security/plugin-signature-verifier.js');
9
10
  const { logger } = require('../utils/logger');
10
11
 
12
+ /**
13
+ * Locate the package's contracts/ directory. This file lives in src/core/ when
14
+ * loaded from source but is inlined into dist/index.cjs when bundled, so
15
+ * __dirname differs between the two layouts.
16
+ *
17
+ * @returns {string} Absolute path to the contracts directory
18
+ */
19
+ function resolveContractsDir() {
20
+ const candidates = [
21
+ path.join(__dirname, '../../contracts'),
22
+ path.join(__dirname, '../contracts'),
23
+ ];
24
+ return candidates.find((dir) => fs.existsSync(dir)) || candidates[0];
25
+ }
26
+
11
27
  class PluginRegistry {
12
28
  constructor(options = {}) {
13
29
  this._plugins = new Map();
@@ -27,7 +43,43 @@ class PluginRegistry {
27
43
  ? options.validateContractSchema
28
44
  : true; // Enable schema validation by default
29
45
 
30
- // Initialize Ajv for contract schema validation
46
+ // Environment-aware signature verification defaults
47
+ const isDevelopment = process.env.NODE_ENV === 'development';
48
+ const isProduction = process.env.NODE_ENV === 'production';
49
+
50
+ this._signatureVerifier = createPluginVerifier({
51
+ enabled:
52
+ options.verifySignatures !== undefined
53
+ ? options.verifySignatures
54
+ : isProduction, // Enabled by default in production, disabled in development
55
+ failClosed:
56
+ options.failClosed !== undefined ? options.failClosed : isProduction, // Fail closed in production, fail open in development
57
+ trustedKeysPath: options.trustedKeysPath,
58
+ requireContentDigest: options.requireContentDigest === true,
59
+ });
60
+ // Opt-in: when verification is enabled, refuse plugins registered
61
+ // without a signed manifest (default false for backward compatibility).
62
+ this._requireSignedPlugins = options.requireSignedPlugins === true;
63
+
64
+ // Contracts (and ajv, which validates them) load in the constructor, or
65
+ // on first use with `lazyContracts: true`. The shared registry exported
66
+ // by this module is lazy, so importing the package does not pay for
67
+ // them unless a plugin is registered or validated.
68
+ this._contractsLoaded = false;
69
+ if (!options.lazyContracts) {
70
+ this._ensureContracts();
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Load the contract schema and the stage contracts once.
76
+ * @private
77
+ */
78
+ _ensureContracts() {
79
+ if (this._contractsLoaded) return;
80
+ this._contractsLoaded = true;
81
+
82
+ const Ajv = loadAjv();
31
83
  this._ajv = new Ajv({
32
84
  allErrors: true,
33
85
  verbose: true,
@@ -55,19 +107,6 @@ class PluginRegistry {
55
107
  }
56
108
  }
57
109
 
58
- // Environment-aware signature verification defaults
59
- const isDevelopment = process.env.NODE_ENV === 'development';
60
- const isProduction = process.env.NODE_ENV === 'production';
61
-
62
- this._signatureVerifier = createPluginVerifier({
63
- enabled:
64
- options.verifySignatures !== undefined
65
- ? options.verifySignatures
66
- : isProduction, // Enabled by default in production, disabled in development
67
- failClosed:
68
- options.failClosed !== undefined ? options.failClosed : isProduction, // Fail closed in production, fail open in development
69
- trustedKeysPath: options.trustedKeysPath,
70
- });
71
110
  this._loadContracts();
72
111
  }
73
112
 
@@ -82,8 +121,8 @@ class PluginRegistry {
82
121
  _loadContractSchema() {
83
122
  try {
84
123
  const schemaPath = path.join(
85
- __dirname,
86
- '../../contracts/contract-schema.json',
124
+ resolveContractsDir(),
125
+ 'contract-schema.json',
87
126
  );
88
127
  if (fs.existsSync(schemaPath)) {
89
128
  const schemaContent = fs.readFileSync(schemaPath, 'utf8');
@@ -186,6 +225,7 @@ class PluginRegistry {
186
225
  * @since 2.2.4
187
226
  */
188
227
  checkContractExists(type) {
228
+ this._ensureContracts();
189
229
  return this._contracts.has(type);
190
230
  }
191
231
 
@@ -256,7 +296,7 @@ class PluginRegistry {
256
296
  }
257
297
 
258
298
  _loadContracts() {
259
- const contractsDir = path.join(__dirname, '../../contracts');
299
+ const contractsDir = resolveContractsDir();
260
300
 
261
301
  try {
262
302
  const contractFiles = {
@@ -338,6 +378,7 @@ class PluginRegistry {
338
378
  }
339
379
 
340
380
  _validatePluginContract(category, impl) {
381
+ this._ensureContracts();
341
382
  const contract = this._contracts.get(category);
342
383
  if (!contract) {
343
384
  // Use new warning method when registering a plugin without a contract
@@ -396,7 +437,20 @@ class PluginRegistry {
396
437
  return methods;
397
438
  }
398
439
 
399
- async register(category, name, impl, manifest = null) {
440
+ /**
441
+ * Register a plugin.
442
+ *
443
+ * @param {string} category - Plugin type
444
+ * @param {string} name - Plugin name
445
+ * @param {object} impl - Plugin implementation
446
+ * @param {object|null} [manifest] - Signed manifest; when given, its
447
+ * signature is verified before registration
448
+ * @param {object} [options]
449
+ * @param {object} [options.content] - Plugin content checked against the
450
+ * manifest's contentDigest: `{ files, baseDir, contents }`
451
+ * @returns {Promise<PluginRegistry>}
452
+ */
453
+ async register(category, name, impl, manifest = null, options = {}) {
400
454
  if (!category || typeof category !== 'string') {
401
455
  throw new Error('Category must be a non-empty string');
402
456
  }
@@ -412,7 +466,22 @@ class PluginRegistry {
412
466
 
413
467
  // Verify plugin signature if manifest provided
414
468
  if (manifest) {
415
- await this._verifyPluginSignature(manifest, name);
469
+ await this._verifyPluginSignature(manifest, name, options.content);
470
+ } else if (
471
+ this._requireSignedPlugins &&
472
+ this.isSignatureVerificationEnabled()
473
+ ) {
474
+ this._emitAuditEntry('plugin_verification_failed', {
475
+ pluginName: name,
476
+ signerId: 'unknown',
477
+ version: 'unknown',
478
+ verified: false,
479
+ error: 'missing signed manifest',
480
+ timestamp: new Date().toISOString(),
481
+ });
482
+ throw new Error(
483
+ `Plugin ${name} has no signed manifest and requireSignedPlugins is enabled`,
484
+ );
416
485
  }
417
486
 
418
487
  // Validate plugin against contract
@@ -428,7 +497,7 @@ class PluginRegistry {
428
497
  * @param {object} manifest - Plugin manifest
429
498
  * @param {string} pluginName - Plugin name
430
499
  */
431
- async _verifyPluginSignature(manifest, pluginName) {
500
+ async _verifyPluginSignature(manifest, pluginName, content = undefined) {
432
501
  try {
433
502
  // Extract signature information from manifest
434
503
  if (!manifest.signature || !manifest.signerId) {
@@ -437,12 +506,12 @@ class PluginRegistry {
437
506
  );
438
507
  }
439
508
 
509
+ const args = [manifest, manifest.signature, manifest.signerId];
510
+ if (content) {
511
+ args.push(content);
512
+ }
440
513
  const verificationResult =
441
- await this._signatureVerifier.verifyPluginSignature(
442
- manifest,
443
- manifest.signature,
444
- manifest.signerId,
445
- );
514
+ await this._signatureVerifier.verifyPluginSignature(...args);
446
515
 
447
516
  if (!verificationResult.verified) {
448
517
  throw new Error(
@@ -473,6 +542,35 @@ class PluginRegistry {
473
542
  }
474
543
  }
475
544
 
545
+ /**
546
+ * Whether plugin signature verification is enabled for this registry.
547
+ * @returns {boolean}
548
+ */
549
+ isSignatureVerificationEnabled() {
550
+ return Boolean(this._signatureVerifier && this._signatureVerifier.enabled);
551
+ }
552
+
553
+ /**
554
+ * Whether plugins without a signed manifest are rejected.
555
+ * @returns {boolean}
556
+ */
557
+ requiresSignedPlugins() {
558
+ return this._requireSignedPlugins && this.isSignatureVerificationEnabled();
559
+ }
560
+
561
+ /**
562
+ * Verify a plugin's signed manifest and code before it is loaded (use this
563
+ * before importing plugin files, since importing executes them).
564
+ *
565
+ * @param {string} pluginName - Plugin name (for audit entries)
566
+ * @param {object} manifest - Signed manifest
567
+ * @param {object} content - `{ files, baseDir, contents }`
568
+ * @returns {Promise<void>} Rejects if verification fails
569
+ */
570
+ async verifyPluginContent(pluginName, manifest, content) {
571
+ await this._verifyPluginSignature(manifest, pluginName, content);
572
+ }
573
+
476
574
  /**
477
575
  * Emit audit entry
478
576
  * @param {string} action - Audit action
@@ -533,6 +631,7 @@ class PluginRegistry {
533
631
  const registry = new PluginRegistry({
534
632
  verifySignatures: process.env.NODE_ENV === 'production',
535
633
  failClosed: process.env.NODE_ENV === 'production',
634
+ lazyContracts: true,
536
635
  });
537
636
 
538
637
  // CJS+ESM interop pattern
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * What each pipeline stage must provide, as createRagPipeline and the CLI
5
+ * use it. A plugin needs at least one of `anyOf`; a missing `recommended`
6
+ * method only matters for the command named in its note.
7
+ *
8
+ * Shared by validatePlugin() and the `rag-pipeline init` wizard. The
9
+ * stricter rules pluginRegistry.register() applies (metadata, and the
10
+ * methods in contracts/*.json) are checked by validatePlugin(..., {
11
+ * registry: true }).
12
+ */
13
+ const STAGE_CONTRACTS = Object.freeze({
14
+ loader: {
15
+ anyOf: ['load'],
16
+ recommended: {},
17
+ },
18
+ embedder: {
19
+ anyOf: ['embed', 'embedQuery'],
20
+ recommended: { embed: '`ingest` calls embed() to embed documents' },
21
+ },
22
+ retriever: {
23
+ anyOf: ['retrieve'],
24
+ recommended: { store: '`ingest` calls store() to save vectors' },
25
+ },
26
+ reranker: {
27
+ anyOf: ['rerank'],
28
+ recommended: {},
29
+ },
30
+ llm: {
31
+ anyOf: ['generate'],
32
+ recommended: {},
33
+ },
34
+ });
35
+
36
+ module.exports = { STAGE_CONTRACTS };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const { STAGE_CONTRACTS } = require('./stage-contracts');
4
+
5
+ /**
6
+ * @typedef {Object} PluginValidationResult
7
+ * @property {boolean} valid - True when there are no errors
8
+ * @property {string[]} errors - Problems that stop the plugin from working
9
+ * @property {string[]} warnings - Methods some commands need, e.g. `store`
10
+ * for `rag-pipeline ingest`
11
+ */
12
+
13
+ /**
14
+ * Checks that an object can be used as a pipeline stage, without throwing,
15
+ * so plugin authors can assert on it in their own tests.
16
+ *
17
+ * By default it checks what createRagPipeline() calls. With
18
+ * `{ registry: true }` it also applies the rules pluginRegistry.register()
19
+ * enforces: `metadata` with a string `name` and `version` and a `type`
20
+ * equal to the stage, plus the methods in the stage's contract.
21
+ *
22
+ * @example
23
+ * const { validatePlugin } = require('@devilsdev/rag-pipeline-utils');
24
+ * const result = validatePlugin('llm', myLlm, { registry: true });
25
+ * expect(result.errors).toEqual([]);
26
+ *
27
+ * @param {'loader'|'embedder'|'retriever'|'reranker'|'llm'} stage
28
+ * @param {object} plugin - Plugin instance or object
29
+ * @param {Object} [options]
30
+ * @param {boolean} [options.registry=false] - Also check registry rules
31
+ * @returns {PluginValidationResult}
32
+ */
33
+ function validatePlugin(stage, plugin, options = {}) {
34
+ const contract = STAGE_CONTRACTS[stage];
35
+ if (!contract) {
36
+ return {
37
+ valid: false,
38
+ errors: [
39
+ `Unknown stage "${stage}". Expected one of: ${Object.keys(STAGE_CONTRACTS).join(', ')}`,
40
+ ],
41
+ warnings: [],
42
+ };
43
+ }
44
+
45
+ if (
46
+ plugin === null ||
47
+ (typeof plugin !== 'object' && typeof plugin !== 'function')
48
+ ) {
49
+ return {
50
+ valid: false,
51
+ errors: [`A ${stage} plugin must be an object or class instance`],
52
+ warnings: [],
53
+ };
54
+ }
55
+
56
+ const has = (method) => typeof plugin[method] === 'function';
57
+ const errors = [];
58
+ const warnings = [];
59
+
60
+ if (!contract.anyOf.some(has)) {
61
+ errors.push(
62
+ `A ${stage} needs ${contract.anyOf.map((m) => `${m}()`).join(' or ')}`,
63
+ );
64
+ }
65
+ for (const [method, why] of Object.entries(contract.recommended)) {
66
+ if (!has(method)) warnings.push(`No ${method}() method: ${why}`);
67
+ }
68
+
69
+ if (options.registry) {
70
+ // Reuse the registry's own check so the two cannot drift. It stops at
71
+ // the first problem, so this reports one registry error at a time.
72
+ // eslint-disable-next-line global-require
73
+ const { PluginRegistry } = require('./plugin-registry');
74
+ try {
75
+ new PluginRegistry()._validatePluginContract(stage, plugin);
76
+ } catch (error) {
77
+ const message = `pluginRegistry.register(): ${error.message}`;
78
+ if (!errors.includes(message)) errors.push(message);
79
+ }
80
+ }
81
+
82
+ return { valid: errors.length === 0, errors, warnings };
83
+ }
84
+
85
+ module.exports = { validatePlugin };
@@ -2,26 +2,134 @@
2
2
 
3
3
  const { EventEmitter } = require('events');
4
4
 
5
+ /**
6
+ * Where the built-in prices come from and when they were recorded.
7
+ *
8
+ * The values are the providers' published list prices as copied into this
9
+ * library on `asOf`; they have not been re-verified since and providers
10
+ * change prices often. Pass `pricing` or `customPricing` to CostCalculator
11
+ * to use current or negotiated prices.
12
+ */
13
+ const PRICING_METADATA = Object.freeze({
14
+ asOf: '2026-04-07',
15
+ verified: false,
16
+ currency: 'USD',
17
+ sources: Object.freeze({
18
+ openai: 'https://openai.com/api/pricing/',
19
+ anthropic: 'https://www.anthropic.com/pricing#api',
20
+ cohere: 'https://cohere.com/pricing',
21
+ }),
22
+ note: 'Built-in list prices, not re-verified since asOf. Override with the `pricing` or `customPricing` option.',
23
+ });
24
+
25
+ const OPENAI = PRICING_METADATA.sources.openai;
26
+ const ANTHROPIC = PRICING_METADATA.sources.anthropic;
27
+ const COHERE = PRICING_METADATA.sources.cohere;
28
+ const AS_OF = PRICING_METADATA.asOf;
29
+
5
30
  /**
6
31
  * Built-in pricing table for common LLM and embedding models.
7
- * Prices are in USD per `unit` tokens.
8
- * @type {Record<string, {input: number, output: number, unit: number}>}
32
+ * Prices are in USD per `unit` tokens. Each entry records its `source`
33
+ * (provider pricing page) and `asOf` date.
34
+ * @type {Record<string, {input: number, output: number, unit: number, source: string, asOf: string}>}
9
35
  */
10
36
  const PRICING = {
11
- 'gpt-4': { input: 0.03, output: 0.06, unit: 1000 },
12
- 'gpt-4-turbo': { input: 0.01, output: 0.03, unit: 1000 },
13
- 'gpt-4o': { input: 0.005, output: 0.015, unit: 1000 },
14
- 'gpt-4o-mini': { input: 0.00015, output: 0.0006, unit: 1000 },
15
- 'gpt-3.5-turbo': { input: 0.0005, output: 0.0015, unit: 1000 },
16
- 'claude-3-opus': { input: 0.015, output: 0.075, unit: 1000 },
17
- 'claude-3-sonnet': { input: 0.003, output: 0.015, unit: 1000 },
18
- 'claude-3-haiku': { input: 0.00025, output: 0.00125, unit: 1000 },
19
- 'claude-3.5-sonnet': { input: 0.003, output: 0.015, unit: 1000 },
20
- 'cohere-command': { input: 0.001, output: 0.002, unit: 1000 },
21
- 'text-embedding-3-small': { input: 0.00002, output: 0, unit: 1000 },
22
- 'text-embedding-3-large': { input: 0.00013, output: 0, unit: 1000 },
37
+ 'gpt-4': {
38
+ input: 0.03,
39
+ output: 0.06,
40
+ unit: 1000,
41
+ source: OPENAI,
42
+ asOf: AS_OF,
43
+ },
44
+ 'gpt-4-turbo': {
45
+ input: 0.01,
46
+ output: 0.03,
47
+ unit: 1000,
48
+ source: OPENAI,
49
+ asOf: AS_OF,
50
+ },
51
+ 'gpt-4o': {
52
+ input: 0.005,
53
+ output: 0.015,
54
+ unit: 1000,
55
+ source: OPENAI,
56
+ asOf: AS_OF,
57
+ },
58
+ 'gpt-4o-mini': {
59
+ input: 0.00015,
60
+ output: 0.0006,
61
+ unit: 1000,
62
+ source: OPENAI,
63
+ asOf: AS_OF,
64
+ },
65
+ 'gpt-3.5-turbo': {
66
+ input: 0.0005,
67
+ output: 0.0015,
68
+ unit: 1000,
69
+ source: OPENAI,
70
+ asOf: AS_OF,
71
+ },
72
+ 'claude-3-opus': {
73
+ input: 0.015,
74
+ output: 0.075,
75
+ unit: 1000,
76
+ source: ANTHROPIC,
77
+ asOf: AS_OF,
78
+ },
79
+ 'claude-3-sonnet': {
80
+ input: 0.003,
81
+ output: 0.015,
82
+ unit: 1000,
83
+ source: ANTHROPIC,
84
+ asOf: AS_OF,
85
+ },
86
+ 'claude-3-haiku': {
87
+ input: 0.00025,
88
+ output: 0.00125,
89
+ unit: 1000,
90
+ source: ANTHROPIC,
91
+ asOf: AS_OF,
92
+ },
93
+ 'claude-3.5-sonnet': {
94
+ input: 0.003,
95
+ output: 0.015,
96
+ unit: 1000,
97
+ source: ANTHROPIC,
98
+ asOf: AS_OF,
99
+ },
100
+ 'cohere-command': {
101
+ input: 0.001,
102
+ output: 0.002,
103
+ unit: 1000,
104
+ source: COHERE,
105
+ asOf: AS_OF,
106
+ },
107
+ 'text-embedding-3-small': {
108
+ input: 0.00002,
109
+ output: 0,
110
+ unit: 1000,
111
+ source: OPENAI,
112
+ asOf: AS_OF,
113
+ },
114
+ 'text-embedding-3-large': {
115
+ input: 0.00013,
116
+ output: 0,
117
+ unit: 1000,
118
+ source: OPENAI,
119
+ asOf: AS_OF,
120
+ },
23
121
  };
24
122
 
123
+ /**
124
+ * Canonical form used for model-name matching: lower case, with '.' and '_'
125
+ * treated like '-' so "claude-3-5-sonnet-20241022" matches "claude-3.5-sonnet".
126
+ * @param {string} name
127
+ * @returns {string}
128
+ */
129
+ function canonical(name) {
130
+ return String(name).toLowerCase().replace(/[._]/g, '-');
131
+ }
132
+
25
133
  /**
26
134
  * Maps token counts to cost estimates by provider and model.
27
135
  *
@@ -33,34 +141,80 @@ const PRICING = {
33
141
  class CostCalculator extends EventEmitter {
34
142
  /**
35
143
  * @param {object} [options]
144
+ * @param {Record<string, {input: number, output: number, unit?: number, source?: string, asOf?: string}>} [options.pricing]
145
+ * Replaces the built-in pricing table entirely (only these models are known).
36
146
  * @param {Record<string, {input: number, output: number, unit: number}>} [options.customPricing]
37
- * Additional or override model pricing entries.
147
+ * Additional or override model pricing entries, merged over the base table.
38
148
  */
39
149
  constructor(options = {}) {
40
150
  super();
41
- this.pricing = { ...PRICING, ...(options.customPricing || {}) };
151
+ const base = options.pricing ? {} : PRICING;
152
+ const overrides = {
153
+ ...(options.pricing || {}),
154
+ ...(options.customPricing || {}),
155
+ };
156
+ this.pricing = { ...base };
157
+ for (const [model, entry] of Object.entries(overrides)) {
158
+ this.pricing[model] = {
159
+ unit: 1000,
160
+ source: 'custom',
161
+ asOf: null,
162
+ ...entry,
163
+ };
164
+ }
165
+ /** @private Models already warned about as unknown */
166
+ this._warnedModels = new Set();
42
167
  }
43
168
 
44
169
  /**
45
170
  * Resolve a model key from the pricing table.
46
- * Tries exact match (case-insensitive), then partial match.
171
+ * Tries an exact match (case-insensitive, '.' and '-' equivalent), then the
172
+ * longest known key contained in the model name (so dated snapshots such
173
+ * as "gpt-4o-mini-2024-07-18" resolve to "gpt-4o-mini", not "gpt-4"), then
174
+ * a known key that contains the model name.
47
175
  * @param {string} model
48
176
  * @returns {string|null} The matched pricing key or null.
49
177
  * @private
50
178
  */
51
179
  _resolveModel(model) {
52
- const lower = model.toLowerCase();
180
+ if (model == null) return null;
181
+ const wanted = canonical(model);
53
182
  const keys = Object.keys(this.pricing);
54
183
 
55
- // Exact case-insensitive match
56
- const exact = keys.find((k) => k.toLowerCase() === lower);
184
+ const exact = keys.find((k) => canonical(k) === wanted);
57
185
  if (exact) return exact;
58
186
 
59
- // Partial match — model string contains a known key or vice-versa
60
- const partial = keys.find(
61
- (k) => lower.includes(k.toLowerCase()) || k.toLowerCase().includes(lower),
62
- );
63
- return partial || null;
187
+ let best = null;
188
+ for (const k of keys) {
189
+ if (wanted.includes(canonical(k)) && (!best || k.length > best.length)) {
190
+ best = k;
191
+ }
192
+ }
193
+ if (best) return best;
194
+
195
+ const containing = keys.find((k) => canonical(k).includes(wanted));
196
+ return containing || null;
197
+ }
198
+
199
+ /**
200
+ * Emit a one-time warning for a model with no known price.
201
+ * Listeners on 'warning' receive `{ type: 'unknown-model', model, message }`;
202
+ * without a listener the warning goes to process.emitWarning.
203
+ * @param {string} model
204
+ * @private
205
+ */
206
+ _warnUnknownModel(model) {
207
+ const key = String(model);
208
+ if (this._warnedModels.has(key)) return;
209
+ this._warnedModels.add(key);
210
+ const message =
211
+ `No pricing known for model "${key}"; cost reported as 0. ` +
212
+ 'Pass `pricing` or `customPricing` to CostCalculator to set a price.';
213
+ if (this.listenerCount('warning') > 0) {
214
+ this.emit('warning', { type: 'unknown-model', model: key, message });
215
+ } else {
216
+ process.emitWarning(message, { code: 'RAG_UNKNOWN_MODEL_PRICING' });
217
+ }
64
218
  }
65
219
 
66
220
  /**
@@ -70,12 +224,14 @@ class CostCalculator extends EventEmitter {
70
224
  * @param {number} inputTokens - Number of input / prompt tokens.
71
225
  * @param {number} [outputTokens=0] - Number of output / completion tokens.
72
226
  * @returns {{inputCost: number, outputCost: number, totalCost: number, model: string,
73
- * inputTokens: number, outputTokens: number, currency: string, warning?: string}}
227
+ * inputTokens: number, outputTokens: number, currency: string,
228
+ * pricingSource?: string, pricingAsOf?: string|null, warning?: string}}
74
229
  */
75
230
  estimate(model, inputTokens, outputTokens = 0) {
76
231
  const resolvedKey = this._resolveModel(model);
77
232
 
78
233
  if (!resolvedKey) {
234
+ this._warnUnknownModel(model);
79
235
  return {
80
236
  inputCost: 0,
81
237
  outputCost: 0,
@@ -89,8 +245,9 @@ class CostCalculator extends EventEmitter {
89
245
  }
90
246
 
91
247
  const price = this.pricing[resolvedKey];
92
- const inputCost = (inputTokens / price.unit) * price.input;
93
- const outputCost = (outputTokens / price.unit) * price.output;
248
+ const unit = price.unit || 1000;
249
+ const inputCost = (inputTokens / unit) * price.input;
250
+ const outputCost = (outputTokens / unit) * price.output;
94
251
  const totalCost = inputCost + outputCost;
95
252
 
96
253
  return {
@@ -101,6 +258,8 @@ class CostCalculator extends EventEmitter {
101
258
  inputTokens,
102
259
  outputTokens,
103
260
  currency: 'USD',
261
+ pricingSource: price.source || 'custom',
262
+ pricingAsOf: price.asOf ?? null,
104
263
  };
105
264
  }
106
265
 
@@ -108,7 +267,7 @@ class CostCalculator extends EventEmitter {
108
267
  * Register custom model pricing.
109
268
  *
110
269
  * @param {string} model - Model identifier.
111
- * @param {{input: number, output: number, unit: number}} pricing - Pricing definition.
270
+ * @param {{input: number, output: number, unit?: number, source?: string, asOf?: string}} pricing - Pricing definition.
112
271
  */
113
272
  addModel(model, pricing) {
114
273
  if (
@@ -120,7 +279,13 @@ class CostCalculator extends EventEmitter {
120
279
  'Pricing must include numeric "input" and "output" fields',
121
280
  );
122
281
  }
123
- this.pricing[model] = { unit: 1000, ...pricing };
282
+ this.pricing[model] = {
283
+ unit: 1000,
284
+ source: 'custom',
285
+ asOf: null,
286
+ ...pricing,
287
+ };
288
+ this._warnedModels.clear();
124
289
  }
125
290
 
126
291
  /**
@@ -135,12 +300,20 @@ class CostCalculator extends EventEmitter {
135
300
  * Retrieve pricing for a specific model.
136
301
  *
137
302
  * @param {string} model - Model identifier (supports fuzzy resolution).
138
- * @returns {{input: number, output: number, unit: number}|null}
303
+ * @returns {{input: number, output: number, unit: number, source?: string, asOf?: string|null}|null}
139
304
  */
140
305
  getModelPricing(model) {
141
306
  const key = this._resolveModel(model);
142
307
  return key ? { ...this.pricing[key] } : null;
143
308
  }
309
+
310
+ /**
311
+ * Provenance of the built-in pricing table.
312
+ * @returns {typeof PRICING_METADATA}
313
+ */
314
+ static getPricingMetadata() {
315
+ return PRICING_METADATA;
316
+ }
144
317
  }
145
318
 
146
- module.exports = { CostCalculator, PRICING };
319
+ module.exports = { CostCalculator, PRICING, PRICING_METADATA };