@devilsdev/rag-pipeline-utils 1.0.1 → 2.0.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.
- package/.codecov.yml +21 -0
- package/.github/workflows/ci.yml +18 -0
- package/.husky/pre-commit +5 -0
- package/CHANGELOG.md +33 -0
- package/__tests__/fixtures/.ragrc.json +8 -0
- package/__tests__/fixtures/invalid-ragrc.json +4 -0
- package/__tests__/fixtures/src/mocks/openai-embedder.js +16 -0
- package/__tests__/fixtures/src/mocks/openai-llm.js +9 -0
- package/__tests__/fixtures/src/mocks/pdf-loader.js +17 -0
- package/__tests__/fixtures/src/mocks/pinecone-retriever.js +26 -0
- package/__tests__/fixtures/valid-ragrc.json +16 -0
- package/__tests__/integration/cli/config-flow.test.js +84 -15
- package/__tests__/integration/config/load-config.test.js +45 -30
- package/__tests__/unit/config/validate-schema.test.js +121 -0
- package/__tests__/unit/reranker/llm-reranker.test.js +9 -33
- package/__tests__/unit/reranker/reranker.enriched.test.js +19 -0
- package/__tests__/unit/reranker/reranker.fallback.test.js +15 -0
- package/__tests__/unit/reranker/reranker.snapshot.test.js +9 -11
- package/__tests__/unit/reranker/reranker.structured-output.test.js +17 -0
- package/bin/cli.js +49 -155
- package/package.json +17 -2
- package/plugin.config.json +14 -0
- package/scripts/banner-injector.js +89 -0
- package/scripts/ci-runner.js +51 -0
- package/scripts/repair-fixtures.js +30 -0
- package/scripts/setup.js +26 -2
- package/scripts/verify-fixture-mocks.js +45 -0
- package/scripts/verify-fixtures.js +36 -0
- package/src/config/load-config.js +19 -21
- package/src/config/load-plugin-config.js +56 -0
- package/src/config/validate-plugin-schema.js +28 -0
- package/src/config/validate-schema.js +88 -30
- package/src/core/create-pipeline.js +40 -26
- package/src/core/plugin-contracts.js +21 -0
- package/src/core/plugin-registry.js +4 -17
- package/src/mocks/openai-embedder.js +30 -0
- package/src/mocks/openai-llm.js +18 -0
- package/src/mocks/pdf-loader.js +17 -0
- package/src/mocks/pinecone-retriever.js +25 -0
- package/src/reranker/llm-reranker.js +28 -43
- package/src/utils/ci/diagnostic-reporter.js +36 -0
- package/src/utils/validate-plugin-contract.js +36 -0
- package/.github/workflows/release.yml +0 -20
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: __tests__/unit/reranker/reranker.structured-output.test.js
|
|
3
|
+
* Version: 1.0.0
|
|
4
|
+
* Description: Validates structured LLM output parsing
|
|
5
|
+
* Author: @devilsdev
|
|
6
|
+
*/
|
|
7
|
+
import { LLMReranker } from '../../../src/reranker/llm-reranker.js';
|
|
8
|
+
|
|
9
|
+
test('parses structured LLM JSON result', async () => {
|
|
10
|
+
const mockLLM = { async generate() {
|
|
11
|
+
return JSON.stringify({ ranking: [0, 2, 1] });
|
|
12
|
+
}};
|
|
13
|
+
const docs = [{ text: 'Alpha' }, { text: 'Beta' }, { text: 'Gamma' }];
|
|
14
|
+
const reranker = new LLMReranker({ llm: mockLLM });
|
|
15
|
+
const result = await reranker.rerank('Which is alpha?', docs);
|
|
16
|
+
expect(result[0].text).toBe('Alpha');
|
|
17
|
+
});
|
package/bin/cli.js
CHANGED
|
@@ -1,168 +1,62 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Version:
|
|
5
|
-
*
|
|
6
|
-
* Description: CLI interface for rag-pipeline utilities with loaders, reranker, and evaluation
|
|
2
|
+
* Version: 1.1.0
|
|
3
|
+
* Description: Final CLI entrypoint with dynamic plugin loading and full config validation
|
|
7
4
|
* Author: Ali Kahwaji
|
|
8
5
|
*/
|
|
9
6
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
7
|
+
import { resolve } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
12
9
|
import { loadRagConfig } from '../src/config/load-config.js';
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import { LLMReranker } from '../src/reranker/llm-reranker.js';
|
|
10
|
+
import { loadPluginsFromJson } from '../src/config/load-plugin-config.js';
|
|
11
|
+
import registry from '../src/core/plugin-registry.js';
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import { DirectoryLoader } from '../src/loader/directory-loader.js';
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = resolve(__filename, '..');
|
|
15
|
+
const CONFIG_PATH = resolve('.ragrc.json');
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
async load(filePath) {
|
|
25
|
-
return [{ chunk: () => [`Test loaded: ${filePath}`] }];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
class OpenAIEmbedder {
|
|
29
|
-
async embed(chunks) {
|
|
30
|
-
return chunks.map((text, i) => ({ id: `v${i}`, values: [0.1, 0.2, 0.3], metadata: { text } }));
|
|
31
|
-
}
|
|
32
|
-
async embedQuery(prompt) {
|
|
33
|
-
return [0.1, 0.2, 0.3];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
class PineconeRetriever {
|
|
37
|
-
async store() {}
|
|
38
|
-
async retrieve() {
|
|
39
|
-
return [
|
|
40
|
-
{ id: 'a', text: 'Chunk about pine trees', metadata: {} },
|
|
41
|
-
{ id: 'b', text: 'Chunk about vectors', metadata: {} },
|
|
42
|
-
{ id: 'c', text: 'Chunk about databases', metadata: {} }
|
|
43
|
-
];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
class OpenAILLM {
|
|
47
|
-
async generate(prompt, context) {
|
|
48
|
-
return `Generated answer using: ${context.map(d => d.text).join(', ')}`;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const command = args[0];
|
|
51
19
|
|
|
52
|
-
|
|
53
|
-
registry.register('loader', 'pdf', new PDFLoader());
|
|
54
|
-
registry.register('loader', 'markdown', new MarkdownLoader());
|
|
55
|
-
registry.register('loader', 'html', new HTMLLoader());
|
|
56
|
-
registry.register('loader', 'csv', new CSVLoader());
|
|
57
|
-
registry.register('loader', 'directory', new DirectoryLoader());
|
|
58
|
-
registry.register('embedder', 'openai', new OpenAIEmbedder());
|
|
59
|
-
registry.register('retriever', 'pinecone', new PineconeRetriever());
|
|
60
|
-
registry.register('llm', 'openai-gpt-4', new OpenAILLM());
|
|
61
|
-
|
|
62
|
-
const program = new Command();
|
|
63
|
-
program
|
|
64
|
-
.name('rag-pipeline')
|
|
65
|
-
.description('CLI for RAG pipeline utilities')
|
|
66
|
-
.version('0.1.8');
|
|
67
|
-
|
|
68
|
-
function resolveConfig(cliOpts) {
|
|
20
|
+
async function runCLI() {
|
|
69
21
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
22
|
+
const config = loadRagConfig(CONFIG_PATH);
|
|
23
|
+
await loadPluginsFromJson(CONFIG_PATH);
|
|
24
|
+
|
|
25
|
+
switch (command) {
|
|
26
|
+
case 'ingest': {
|
|
27
|
+
const input = args[1];
|
|
28
|
+
const loader = registry.get('loader', Object.keys(config.loader)[0]);
|
|
29
|
+
const embedder = registry.get('embedder', Object.keys(config.embedder)[0]);
|
|
30
|
+
const retriever = registry.get('retriever', Object.keys(config.retriever)[0]);
|
|
31
|
+
|
|
32
|
+
const docs = await loader.load(input);
|
|
33
|
+
const vectors = await embedder.embed(docs);
|
|
34
|
+
await retriever.store(vectors, config.namespace);
|
|
35
|
+
|
|
36
|
+
console.log('Ingestion complete');
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case 'query': {
|
|
40
|
+
const question = args.slice(1).join(' ');
|
|
41
|
+
const embedder = registry.get('embedder', Object.keys(config.embedder)[0]);
|
|
42
|
+
const retriever = registry.get('retriever', Object.keys(config.retriever)[0]);
|
|
43
|
+
const llm = registry.get('llm', Object.keys(config.llm)[0]);
|
|
44
|
+
|
|
45
|
+
const queryVec = await embedder.embedQuery(question);
|
|
46
|
+
const docs = await retriever.search(queryVec, config.namespace);
|
|
47
|
+
const answer = await llm.ask(question, docs);
|
|
48
|
+
|
|
49
|
+
console.log('Answer:', answer);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
default:
|
|
53
|
+
console.error('Unknown command:', command);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
72
56
|
} catch (err) {
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
console.error('Error during execution:', err.message);
|
|
58
|
+
process.exit(1);
|
|
75
59
|
}
|
|
76
60
|
}
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
.command('ingest')
|
|
80
|
-
.argument('<path>', 'Path to document(s)')
|
|
81
|
-
.option('--loader <type>', 'Document loader type')
|
|
82
|
-
.option('--embedder <type>', 'Embedder type')
|
|
83
|
-
.option('--retriever <type>', 'Vector store retriever type')
|
|
84
|
-
.action(async (docPath, opts) => {
|
|
85
|
-
try {
|
|
86
|
-
const config = resolveConfig(opts);
|
|
87
|
-
const pipeline = createRagPipeline(config);
|
|
88
|
-
await pipeline.ingest(docPath);
|
|
89
|
-
console.log('Ingestion complete'); // required by test assertion
|
|
90
|
-
} catch (err) {
|
|
91
|
-
logger.error({ error: err.message }, 'Ingestion failed');
|
|
92
|
-
console.error('Error during ingestion:', err.message);
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
program
|
|
98
|
-
.command('query')
|
|
99
|
-
.argument('<prompt>', 'Prompt to submit')
|
|
100
|
-
.option('--embedder <type>', 'Embedder type')
|
|
101
|
-
.option('--retriever <type>', 'Retriever type')
|
|
102
|
-
.option('--llm <type>', 'LLM type')
|
|
103
|
-
.action(async (prompt, opts) => {
|
|
104
|
-
try {
|
|
105
|
-
const config = resolveConfig(opts);
|
|
106
|
-
const pipeline = createRagPipeline(config);
|
|
107
|
-
const answer = await pipeline.query(prompt);
|
|
108
|
-
console.log('\n Answer:\n', answer);
|
|
109
|
-
} catch (err) {
|
|
110
|
-
logger.error({ error: err.message }, 'Query failed');
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
program
|
|
116
|
-
.command('evaluate')
|
|
117
|
-
.argument('<dataset>', 'Path to JSON file of evaluation cases')
|
|
118
|
-
.option('--embedder <type>', 'Embedder type')
|
|
119
|
-
.option('--retriever <type>', 'Retriever type')
|
|
120
|
-
.option('--llm <type>', 'LLM type')
|
|
121
|
-
.action(async (dataset, opts) => {
|
|
122
|
-
try {
|
|
123
|
-
const config = resolveConfig(opts);
|
|
124
|
-
const results = await evaluateRagDataset(dataset, config);
|
|
125
|
-
const passRate = results.filter(r => r.success).length / results.length;
|
|
126
|
-
const avgBLEU = results.reduce((sum, r) => sum + r.scores.bleu, 0) / results.length;
|
|
127
|
-
const avgROUGE = results.reduce((sum, r) => sum + r.scores.rouge, 0) / results.length;
|
|
128
|
-
|
|
129
|
-
results.forEach((r, i) => {
|
|
130
|
-
console.log(`\nCase ${i + 1}:\nPrompt: ${r.prompt}\nPass: ${r.success}\nBLEU: ${r.scores.bleu.toFixed(2)}\nROUGE: ${r.scores.rouge.toFixed(2)}`);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
console.log(`\n Summary:\nPassed: ${Math.round(passRate * 100)}%\nAvg BLEU: ${avgBLEU.toFixed(2)}\nAvg ROUGE: ${avgROUGE.toFixed(2)}`);
|
|
134
|
-
} catch (err) {
|
|
135
|
-
logger.error({ error: err.message }, 'Evaluation failed');
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
program
|
|
141
|
-
.command('rerank')
|
|
142
|
-
.argument('<prompt>', 'Prompt to rerank context for')
|
|
143
|
-
.option('--retriever <type>', 'Retriever type')
|
|
144
|
-
.option('--llm <type>', 'LLM type')
|
|
145
|
-
.action(async (prompt, opts) => {
|
|
146
|
-
try {
|
|
147
|
-
const config = resolveConfig(opts);
|
|
148
|
-
const retriever = registry.get('retriever', config.retriever);
|
|
149
|
-
const llm = registry.get('llm', config.llm);
|
|
150
|
-
const embedder = registry.get('embedder', config.embedder);
|
|
151
|
-
|
|
152
|
-
const queryVector = await embedder.embedQuery(prompt);
|
|
153
|
-
const docs = await retriever.retrieve(queryVector);
|
|
154
|
-
|
|
155
|
-
const reranker = new LLMReranker({ llm });
|
|
156
|
-
const reranked = await reranker.rerank(prompt, docs);
|
|
157
|
-
|
|
158
|
-
console.log('\n Reranked Results:');
|
|
159
|
-
reranked.forEach((doc, i) => {
|
|
160
|
-
console.log(`\n#${i + 1}: ${doc.text}`);
|
|
161
|
-
});
|
|
162
|
-
} catch (err) {
|
|
163
|
-
logger.error({ error: err.message }, 'Rerank failed');
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
program.parse();
|
|
62
|
+
await runCLI();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devilsdev/rag-pipeline-utils",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A modular toolkit for building RAG (Retrieval-Augmented Generation) pipelines in Node.js",
|
|
5
5
|
"main": "src/core/create-pipeline.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,10 +8,22 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
|
+
"validate-fixtures": "node ./scripts/verify-fixtures.js",
|
|
12
|
+
"setup": "node ./scripts/setup.js",
|
|
11
13
|
"test": "node ./scripts/setup.js && cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
|
12
14
|
"test:watch": "jest --watch",
|
|
13
15
|
"lint": "eslint .",
|
|
14
|
-
"prepare": "husky install"
|
|
16
|
+
"prepare": "husky install",
|
|
17
|
+
"lint-staged": "lint-staged",
|
|
18
|
+
"postinstall": "husky install",
|
|
19
|
+
"verify:mocks": "node scripts/verify-fixture-mocks.js",
|
|
20
|
+
"ci": "node scripts/ci-runner.js"
|
|
21
|
+
},
|
|
22
|
+
"lint-staged": {
|
|
23
|
+
"**/*.{js,ts}": [
|
|
24
|
+
"npm run lint",
|
|
25
|
+
"npm run validate-fixtures"
|
|
26
|
+
]
|
|
15
27
|
},
|
|
16
28
|
"repository": {
|
|
17
29
|
"type": "git",
|
|
@@ -43,17 +55,20 @@
|
|
|
43
55
|
"@semantic-release/npm": "^11.0.3",
|
|
44
56
|
"@semantic-release/release-notes-generator": "^14.0.3",
|
|
45
57
|
"@types/jest": "^29.5.14",
|
|
58
|
+
"codecov": "^3.8.2",
|
|
46
59
|
"cross-env": "^7.0.3",
|
|
47
60
|
"esbuild": "^0.25.2",
|
|
48
61
|
"eslint": "^8.57.1",
|
|
49
62
|
"husky": "^8.0.3",
|
|
50
63
|
"jest": "^29.7.0",
|
|
64
|
+
"lint-staged": "^15.5.1",
|
|
51
65
|
"prettier": "^3.5.3",
|
|
52
66
|
"rimraf": "^6.0.1",
|
|
53
67
|
"semantic-release": "^21.1.2",
|
|
54
68
|
"ts-jest": "^29.3.2"
|
|
55
69
|
},
|
|
56
70
|
"dependencies": {
|
|
71
|
+
"ajv": "^8.17.1",
|
|
57
72
|
"axios": "^1.8.4",
|
|
58
73
|
"chalk": "^5.4.1",
|
|
59
74
|
"commander": "^13.1.0",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"loader": {
|
|
3
|
+
"pdf": "./src/mocks/pdf-loader.js"
|
|
4
|
+
},
|
|
5
|
+
"embedder": {
|
|
6
|
+
"openai": "./src/mocks/openai-embedder.js"
|
|
7
|
+
},
|
|
8
|
+
"retriever": {
|
|
9
|
+
"pinecone": "./src/mocks/pinecone-retriever.js"
|
|
10
|
+
},
|
|
11
|
+
"llm": {
|
|
12
|
+
"openai-gpt-4": "./src/mocks/openai-llm.js"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: scripts/banner-injector.js
|
|
3
|
+
* Description: Script to programmatically inject file-level banners into specified project files
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
* Version: 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import glob from 'glob';
|
|
11
|
+
|
|
12
|
+
const banners = {
|
|
13
|
+
'__tests__/unit/reranker/llm-reranker.test.js': `/**
|
|
14
|
+
* File: __tests__/unit/reranker/llm-reranker.test.js
|
|
15
|
+
* Description: Unit and integration tests with mocks for RAG pipeline
|
|
16
|
+
* Author: Ali Kahwaji
|
|
17
|
+
* Version: 1.0.4
|
|
18
|
+
*/\n\n`,
|
|
19
|
+
|
|
20
|
+
'__tests__/unit/reranker/reranker.snapshot.test.js': `/**
|
|
21
|
+
* File: __tests__/unit/reranker/reranker.snapshot.test.js
|
|
22
|
+
* Description: Snapshot tests for LLM reranker
|
|
23
|
+
* Author: Ali Kahwaji
|
|
24
|
+
* Version: 1.0.4
|
|
25
|
+
*/\n\n`,
|
|
26
|
+
|
|
27
|
+
'__tests__/unit/core/plugin-registry.test.js': `/**
|
|
28
|
+
* File: __tests__/unit/core/plugin-registry.test.js
|
|
29
|
+
* Description: Unit and integration tests with mocks for RAG pipeline
|
|
30
|
+
* Author: Ali Kahwaji
|
|
31
|
+
* Version: 1.0.4
|
|
32
|
+
*/\n\n`,
|
|
33
|
+
|
|
34
|
+
'__tests__/integration/config/load-config.test.js': `/**
|
|
35
|
+
* File: __tests__/integration/config/load-config.test.js
|
|
36
|
+
* Description: Unit and integration tests with mocks for RAG pipeline
|
|
37
|
+
* Author: Ali Kahwaji
|
|
38
|
+
* Version: 1.0.4
|
|
39
|
+
*/\n\n`,
|
|
40
|
+
|
|
41
|
+
'__tests__/integration/cli/config-flow.test.js': `/**
|
|
42
|
+
* File: __tests__/integration/cli/config-flow.test.js
|
|
43
|
+
* Description: Unit and integration tests with mocks for RAG pipeline
|
|
44
|
+
* Author: Ali Kahwaji
|
|
45
|
+
* Version: 1.0.4
|
|
46
|
+
*/\n\n`,
|
|
47
|
+
|
|
48
|
+
'src/mocks/pdf-loader.js': `/**
|
|
49
|
+
* File: src/mocks/pdf-loader.js
|
|
50
|
+
* Description: Unit and integration tests with mocks for RAG pipeline
|
|
51
|
+
* Author: Ali Kahwaji
|
|
52
|
+
* Version: 1.0.4
|
|
53
|
+
*/\n\n`,
|
|
54
|
+
|
|
55
|
+
'src/mocks/openai-embedder.js': `/**
|
|
56
|
+
* File: src/mocks/openai-embedder.js
|
|
57
|
+
* Description: Unit and integration tests with mocks for RAG pipeline
|
|
58
|
+
* Author: Ali Kahwaji
|
|
59
|
+
* Version: 1.0.4
|
|
60
|
+
*/\n\n`,
|
|
61
|
+
|
|
62
|
+
'src/mocks/pinecone-retriever.js': `/**
|
|
63
|
+
* File: src/mocks/pinecone-retriever.js
|
|
64
|
+
* Description: Unit and integration tests with mocks for RAG pipeline
|
|
65
|
+
* Author: Ali Kahwaji
|
|
66
|
+
* Version: 1.0.4
|
|
67
|
+
*/\n\n`
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
Object.entries(banners).forEach(([filePath, banner]) => {
|
|
71
|
+
const resolvedPath = path.resolve(filePath);
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
74
|
+
console.warn(`[SKIPPED] ${filePath} does not exist.`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const originalContent = fs.readFileSync(resolvedPath, 'utf-8');
|
|
79
|
+
|
|
80
|
+
// Skip if banner already present
|
|
81
|
+
if (originalContent.startsWith('/**')) {
|
|
82
|
+
console.log(`[SKIPPED] ${filePath} already contains a banner.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const newContent = banner + originalContent;
|
|
87
|
+
fs.writeFileSync(resolvedPath, newContent, 'utf-8');
|
|
88
|
+
console.log(`[INJECTED] Banner added to ${filePath}`);
|
|
89
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.1.0
|
|
3
|
+
* Description: CI runner for linting, mock validation, testing and coverage
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
* File: scripts/ci-runner.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { pathToFileURL } from 'url';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
|
|
13
|
+
// Entry helpers
|
|
14
|
+
const ROOT = path.resolve();
|
|
15
|
+
const resolveScript = (relPath) => pathToFileURL(path.resolve(ROOT, relPath)).href;
|
|
16
|
+
|
|
17
|
+
const run = (label, command) => {
|
|
18
|
+
console.log(`\n🔧 Running: ${label}`);
|
|
19
|
+
try {
|
|
20
|
+
execSync(command, { stdio: 'inherit', cwd: ROOT });
|
|
21
|
+
console.log(`✅ ${label} passed`);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(`❌ ${label} failed`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const runDynamicModule = async (label, modulePath) => {
|
|
29
|
+
console.log(`\n🔍 Validating: ${label}`);
|
|
30
|
+
try {
|
|
31
|
+
const mod = await import(resolveScript(modulePath));
|
|
32
|
+
if (typeof mod.default === 'function') await mod.default();
|
|
33
|
+
console.log(`✅ ${label} passed`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(`❌ ${label} failed:\n${err.message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
(async () => {
|
|
41
|
+
// Step 1: Lint
|
|
42
|
+
run('Lint', 'npx eslint . --ext .js,.ts');
|
|
43
|
+
|
|
44
|
+
// Step 2: Verify mocks for required interface methods
|
|
45
|
+
await runDynamicModule('Fixture Mock Interface Validation', 'scripts/verify-fixture-mocks.js');
|
|
46
|
+
|
|
47
|
+
// Step 3: Test and coverage
|
|
48
|
+
run('Unit + Integration Tests w/ Coverage', 'cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage');
|
|
49
|
+
|
|
50
|
+
console.log('\n🎉 CI runner completed successfully.\n');
|
|
51
|
+
})();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Description: Auto-repairs mock plugin fixtures for integration tests
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, copyFileSync, existsSync } from 'fs';
|
|
8
|
+
import { resolve, join } from 'path';
|
|
9
|
+
|
|
10
|
+
const MOCKS = [
|
|
11
|
+
'pdf-loader.js',
|
|
12
|
+
'openai-embedder.js',
|
|
13
|
+
'pinecone-retriever.js',
|
|
14
|
+
'openai-llm.js'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const SRC_FIXTURES = resolve('__tests__/fixtures/src/mocks');
|
|
18
|
+
const DEST_FIXTURES = resolve('__tests__/__temp__/cli-config-fallback/src/mocks');
|
|
19
|
+
|
|
20
|
+
mkdirSync(DEST_FIXTURES, { recursive: true });
|
|
21
|
+
|
|
22
|
+
MOCKS.forEach((file) => {
|
|
23
|
+
const src = join(SRC_FIXTURES, file);
|
|
24
|
+
const dest = join(DEST_FIXTURES, file);
|
|
25
|
+
if (!existsSync(src)) throw new Error(`[FIXTURE MISSING] ${src}`);
|
|
26
|
+
copyFileSync(src, dest);
|
|
27
|
+
console.log(`[COPIED] ${file}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log('✅ Fixture mocks successfully repaired.');
|
package/scripts/setup.js
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.1.0
|
|
3
|
+
* Description: Test setup script to ensure fixtures exist and validate mocks before running tests.
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import fs from 'fs';
|
|
2
|
-
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
// Resolve __dirname in ES module scope
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const fixturePath = path.resolve(__dirname, '../__tests__/fixtures/sample.pdf');
|
|
3
17
|
|
|
18
|
+
// Ensure PDF test fixture
|
|
4
19
|
if (!fs.existsSync(fixturePath)) {
|
|
5
|
-
fs.mkdirSync(
|
|
20
|
+
fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
|
|
6
21
|
fs.writeFileSync(fixturePath, 'Dummy PDF content for test');
|
|
22
|
+
console.log('[setup] Created sample.pdf fixture');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate plugin fixture presence
|
|
26
|
+
try {
|
|
27
|
+
execSync('node ./scripts/verify-fixtures.js', { stdio: 'inherit' });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('[setup] Fixture verification failed. Please check missing mocks.');
|
|
30
|
+
process.exit(1);
|
|
7
31
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Description: Verifies that plugin mock implementations expose required methods
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdirSync, statSync } from 'fs';
|
|
8
|
+
import { resolve, join, extname } from 'path';
|
|
9
|
+
import { pathToFileURL } from 'url';
|
|
10
|
+
|
|
11
|
+
const MOCKS_DIR = resolve('__tests__/fixtures/src/mocks');
|
|
12
|
+
|
|
13
|
+
const pluginContracts = {
|
|
14
|
+
'openai-embedder.js': ['embed', 'embedQuery'],
|
|
15
|
+
'openai-llm.js': ['ask'],
|
|
16
|
+
'pdf-loader.js': ['load'],
|
|
17
|
+
'pinecone-retriever.js': ['store', 'search'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const failures = [];
|
|
21
|
+
|
|
22
|
+
for (const file of readdirSync(MOCKS_DIR)) {
|
|
23
|
+
const fullPath = join(MOCKS_DIR, file);
|
|
24
|
+
const isFile = statSync(fullPath).isFile();
|
|
25
|
+
if (!isFile || extname(file) !== '.js') continue;
|
|
26
|
+
|
|
27
|
+
const expectedMethods = pluginContracts[file];
|
|
28
|
+
if (!expectedMethods) continue;
|
|
29
|
+
|
|
30
|
+
const mod = await import(pathToFileURL(fullPath).href);
|
|
31
|
+
const plugin = new mod.default();
|
|
32
|
+
|
|
33
|
+
for (const method of expectedMethods) {
|
|
34
|
+
if (typeof plugin[method] !== 'function') {
|
|
35
|
+
failures.push(`❌ ${file} failed: [${file}] missing method: ${method}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (failures.length > 0) {
|
|
41
|
+
failures.forEach(msg => console.error(msg));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
} else {
|
|
44
|
+
console.log('✅ All plugin mocks verified.');
|
|
45
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 1.0.0
|
|
3
|
+
* Description: Script to verify presence of all expected fixture plugins before running integration tests.
|
|
4
|
+
* Author: Ali Kahwaji
|
|
5
|
+
* File: scripts/verify-fixtures.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
|
|
11
|
+
const REQUIRED_FIXTURES = [
|
|
12
|
+
'__tests__/fixtures/sample.pdf',
|
|
13
|
+
'__tests__/fixtures/.ragrc.json',
|
|
14
|
+
'__tests__/fixtures/src/mocks/pdf-loader.js',
|
|
15
|
+
'__tests__/fixtures/src/mocks/openai-embedder.js',
|
|
16
|
+
'__tests__/fixtures/src/mocks/pinecone-retriever.js',
|
|
17
|
+
'__tests__/fixtures/src/mocks/openai-llm.js'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
let missing = [];
|
|
21
|
+
for (const file of REQUIRED_FIXTURES) {
|
|
22
|
+
const fullPath = resolve(file);
|
|
23
|
+
if (!existsSync(fullPath)) {
|
|
24
|
+
missing.push(fullPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (missing.length) {
|
|
29
|
+
console.error('[ERROR] Missing test fixture files:');
|
|
30
|
+
for (const f of missing) {
|
|
31
|
+
console.error(' -', f);
|
|
32
|
+
}
|
|
33
|
+
process.exit(1);
|
|
34
|
+
} else {
|
|
35
|
+
console.log('[OK] All fixture files are present.');
|
|
36
|
+
}
|
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version:
|
|
3
|
-
*
|
|
4
|
-
* Description: Loader and validator for `.ragrc.json` configuration files
|
|
2
|
+
* Version: 1.1.0
|
|
3
|
+
* Description: Loads validated .ragrc.json config from disk
|
|
5
4
|
* Author: Ali Kahwaji
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import fs from 'fs';
|
|
9
8
|
import path from 'path';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
const DEFAULT_CONFIG_FILE = '.ragrc.json';
|
|
9
|
+
import { validateRagrcSchema } from './validate-schema.js';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
19
|
-
* @throws {Error} - If file not found or validation fails
|
|
13
|
+
* Loads and validates a RAG config file.
|
|
14
|
+
* @param {string} configPath - Absolute path to .ragrc.json
|
|
15
|
+
* @returns {object} Parsed config object
|
|
16
|
+
* @throws {Error} If file is missing or invalid
|
|
20
17
|
*/
|
|
21
|
-
export function loadRagConfig(configPath
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!fs.existsSync(fullPath)) {
|
|
25
|
-
throw new Error(`Config file not found: ${fullPath}`);
|
|
18
|
+
export function loadRagConfig(configPath) {
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
const raw = fs.readFileSync(
|
|
29
|
-
const
|
|
23
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
const result = validateRagrcSchema(parsed);
|
|
27
|
+
if (!result.valid) {
|
|
28
|
+
logger.error({ errors: result.errors }, 'Invalid .ragrc.json schema');
|
|
29
|
+
throw new Error('Config validation failed');
|
|
30
|
+
}
|
|
32
31
|
|
|
33
|
-
return
|
|
32
|
+
return parsed;
|
|
34
33
|
}
|
|
35
|
-
|