rlhf-feedback-loop 0.5.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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/adapters/README.md +8 -0
- package/adapters/amp/skills/rlhf-feedback/SKILL.md +20 -0
- package/adapters/chatgpt/INSTALL.md +80 -0
- package/adapters/chatgpt/openapi.yaml +292 -0
- package/adapters/claude/.mcp.json +8 -0
- package/adapters/codex/config.toml +4 -0
- package/adapters/gemini/function-declarations.json +95 -0
- package/adapters/mcp/server-stdio.js +444 -0
- package/bin/cli.js +167 -0
- package/config/mcp-allowlists.json +29 -0
- package/config/policy-bundles/constrained-v1.json +53 -0
- package/config/policy-bundles/default-v1.json +80 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/subagent-profiles.json +32 -0
- package/openapi/openapi.yaml +292 -0
- package/package.json +91 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +31 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +57 -0
- package/plugins/gemini-extension/INSTALL.md +74 -0
- package/plugins/gemini-extension/gemini_prompt.txt +10 -0
- package/plugins/gemini-extension/tool_contract.json +28 -0
- package/scripts/billing.js +471 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/code-reasoning.js +307 -0
- package/scripts/context-engine.js +547 -0
- package/scripts/contextfs.js +513 -0
- package/scripts/contract-audit.js +198 -0
- package/scripts/dpo-optimizer.js +208 -0
- package/scripts/export-dpo-pairs.js +316 -0
- package/scripts/export-training.js +448 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +838 -0
- package/scripts/feedback-schema.js +300 -0
- package/scripts/feedback-to-memory.js +165 -0
- package/scripts/feedback-to-rules.js +109 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/hybrid-feedback-context.js +676 -0
- package/scripts/intent-router.js +164 -0
- package/scripts/mcp-policy.js +92 -0
- package/scripts/meta-policy.js +194 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/prove-adapters.js +364 -0
- package/scripts/prove-attribution.js +364 -0
- package/scripts/prove-automation.js +393 -0
- package/scripts/prove-data-quality.js +219 -0
- package/scripts/prove-intelligence.js +256 -0
- package/scripts/prove-lancedb.js +370 -0
- package/scripts/prove-loop-closure.js +255 -0
- package/scripts/prove-rlaif.js +404 -0
- package/scripts/prove-subway-upgrades.js +250 -0
- package/scripts/prove-training-export.js +324 -0
- package/scripts/prove-v2-milestone.js +273 -0
- package/scripts/prove-v3-milestone.js +381 -0
- package/scripts/rlaif-self-audit.js +123 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/self-heal.js +127 -0
- package/scripts/self-healing-check.js +111 -0
- package/scripts/skill-quality-tracker.js +284 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +29 -0
- package/scripts/thompson-sampling.js +331 -0
- package/scripts/train_from_feedback.py +914 -0
- package/scripts/validate-feedback.js +580 -0
- package/scripts/vector-store.js +100 -0
- package/src/api/server.js +497 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Engine
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Dropbox Dash's architecture for intelligent context retrieval.
|
|
5
|
+
* Pre-computes knowledge bundles from project docs, routes queries to relevant
|
|
6
|
+
* context, scores retrieval quality, and manages prompt templates.
|
|
7
|
+
*
|
|
8
|
+
* Key insight: instead of agents reading 100+ docs at runtime, pre-compute
|
|
9
|
+
* topical bundles and route queries to the most relevant subset. This reduces
|
|
10
|
+
* MCP tool calls and context window consumption.
|
|
11
|
+
*
|
|
12
|
+
* Ported from Subway_RN_Demo/scripts/context-engine.js for rlhf-feedback-loop.
|
|
13
|
+
* PATH: PROJECT_ROOT = path.join(__dirname, '..') — 1 level up from scripts/
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Default paths
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
27
|
+
const DEFAULT_DOCS_DIR = path.join(PROJECT_ROOT, 'docs');
|
|
28
|
+
const CONTEXT_ENGINE_DIR = path.join(PROJECT_ROOT, '.claude', 'context-engine');
|
|
29
|
+
const DEFAULT_INDEX_PATH = path.join(CONTEXT_ENGINE_DIR, 'knowledge-index.json');
|
|
30
|
+
const DEFAULT_QUALITY_LOG_PATH = path.join(CONTEXT_ENGINE_DIR, 'quality-log.json');
|
|
31
|
+
const DEFAULT_REGISTRY_PATH = path.join(CONTEXT_ENGINE_DIR, 'prompt-registry.json');
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Category detection rules (from filename patterns)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
// Order: specific domain rules first, broader categories last.
|
|
38
|
+
// This prevents "ANDROID_BUILD" matching BUILD→ci-cd before ANDROID→mobile-dev.
|
|
39
|
+
const CATEGORY_RULES = [
|
|
40
|
+
{ category: 'mobile-dev', pattern: /ANDROID|IOS|EXPO|TURBOMODULE|DEVICE|METRO|MMKV/i },
|
|
41
|
+
{ category: 'mcp-ai', pattern: /MCP|CONTEXT7|CLAUDE|AGENTIC|AGENT|(?:^|_)AI(?:_|\.)|MEMORY/i },
|
|
42
|
+
{ category: 'security', pattern: /SECURITY|CVE|CODEQL|INJECTION|AUDIT|PERMISSION/i },
|
|
43
|
+
{ category: 'testing', pattern: /TEST|COVERAGE|REASSURE|RNTL|MAESTRO|PERF/i },
|
|
44
|
+
{ category: 'ado-git', pattern: /(?:^|_)ADO(?:_|\.)|AZURE|PR_|SQUASH|BRANCH|GITFLOW|GIT_/i },
|
|
45
|
+
{ category: 'architecture', pattern: /ARCHITECTURE|FEATURE|PROJECT_STRUCTURE|PLUGIN|REDUX|CART/i },
|
|
46
|
+
{ category: 'ci-cd', pattern: /(?:^|_)CI(?:_|\.)|(?:^|_)CD(?:_|\.)|BUILD|WORKFLOW|PIPELINE|FIREBASE|(?:^|_)ACT(?:_|\.)/i },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// MCP Consolidation Manifest
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
const TOOL_CONSOLIDATION = {
|
|
54
|
+
'context:retrieve': {
|
|
55
|
+
sources: ['context7', 'knowledge-index'],
|
|
56
|
+
description: 'Unified context retrieval',
|
|
57
|
+
},
|
|
58
|
+
'memory:query': {
|
|
59
|
+
sources: ['shieldcortex', 'memory-files'],
|
|
60
|
+
description: 'Unified memory access',
|
|
61
|
+
},
|
|
62
|
+
'quality:check': {
|
|
63
|
+
sources: ['sonarqube', 'eslint', 'jest'],
|
|
64
|
+
description: 'Unified quality gate',
|
|
65
|
+
},
|
|
66
|
+
'docs:lookup': {
|
|
67
|
+
sources: ['context7', 'knowledge-bundles'],
|
|
68
|
+
description: 'Documentation lookup',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Utility: ensure directory exists
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function ensureDir(dirPath) {
|
|
77
|
+
if (!fs.existsSync(dirPath)) {
|
|
78
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Knowledge Bundle Builder
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Determine the category for a doc file based on its filename.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} filename - The filename (e.g., "CI_FIXES.md")
|
|
90
|
+
* @returns {string} Category string (e.g., "ci-cd", "testing", "general")
|
|
91
|
+
*/
|
|
92
|
+
function categorizeDoc(filename) {
|
|
93
|
+
for (const rule of CATEGORY_RULES) {
|
|
94
|
+
if (rule.pattern.test(filename)) {
|
|
95
|
+
return rule.category;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return 'general';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extract a summary from a markdown file: title + first 3 non-empty lines after it.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} filePath - Absolute path to the markdown file
|
|
105
|
+
* @returns {{ title: string, summary: string }} Extracted title and summary
|
|
106
|
+
*/
|
|
107
|
+
function extractDocSummary(filePath) {
|
|
108
|
+
let content;
|
|
109
|
+
try {
|
|
110
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
111
|
+
} catch {
|
|
112
|
+
return { title: path.basename(filePath, '.md'), summary: '' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const lines = content.split('\n');
|
|
116
|
+
let title = path.basename(filePath, '.md');
|
|
117
|
+
let titleLineIndex = -1;
|
|
118
|
+
|
|
119
|
+
// Find first heading line
|
|
120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
121
|
+
const trimmed = lines[i].trim();
|
|
122
|
+
if (trimmed.startsWith('#')) {
|
|
123
|
+
title = trimmed.replace(/^#+\s*/, '');
|
|
124
|
+
titleLineIndex = i;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Collect first 3 non-empty lines after the title
|
|
130
|
+
const summaryLines = [];
|
|
131
|
+
const startIdx = titleLineIndex + 1;
|
|
132
|
+
for (let i = startIdx; i < lines.length && summaryLines.length < 3; i++) {
|
|
133
|
+
const trimmed = lines[i].trim();
|
|
134
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
135
|
+
summaryLines.push(trimmed);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { title, summary: summaryLines.join(' ') };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Scan a docs directory and build a pre-computed knowledge index.
|
|
144
|
+
*
|
|
145
|
+
* Groups markdown files into topical bundles by category, extracts titles
|
|
146
|
+
* and summaries, and writes the index to disk for fast runtime lookup.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} [docsDir] - Path to the docs directory (default: project docs/)
|
|
149
|
+
* @param {string} [outputPath] - Path to write the index JSON (default: .claude/context-engine/knowledge-index.json)
|
|
150
|
+
* @returns {{ bundles: object, totalDocs: number, generatedAt: string }} The generated index
|
|
151
|
+
*/
|
|
152
|
+
function buildKnowledgeIndex(docsDir, outputPath) {
|
|
153
|
+
const docs = docsDir || DEFAULT_DOCS_DIR;
|
|
154
|
+
const output = outputPath || DEFAULT_INDEX_PATH;
|
|
155
|
+
const bundles = {};
|
|
156
|
+
|
|
157
|
+
// Scan for .md files
|
|
158
|
+
let files;
|
|
159
|
+
try {
|
|
160
|
+
files = fs.readdirSync(docs).filter((f) => f.endsWith('.md'));
|
|
161
|
+
} catch {
|
|
162
|
+
files = [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
const filePath = path.join(docs, file);
|
|
167
|
+
const category = categorizeDoc(file);
|
|
168
|
+
const { title, summary } = extractDocSummary(filePath);
|
|
169
|
+
|
|
170
|
+
if (!bundles[category]) {
|
|
171
|
+
bundles[category] = {
|
|
172
|
+
category,
|
|
173
|
+
docs: [],
|
|
174
|
+
keywords: [],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const doc = {
|
|
179
|
+
filename: file,
|
|
180
|
+
title,
|
|
181
|
+
summary,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
bundles[category].docs.push(doc);
|
|
185
|
+
|
|
186
|
+
// Extract keywords from title and filename
|
|
187
|
+
const words = `${title} ${file.replace(/[._-]/g, ' ')}`
|
|
188
|
+
.toLowerCase()
|
|
189
|
+
.split(/\s+/)
|
|
190
|
+
.filter((w) => w.length > 2 && w !== 'md');
|
|
191
|
+
|
|
192
|
+
for (const word of words) {
|
|
193
|
+
if (!bundles[category].keywords.includes(word)) {
|
|
194
|
+
bundles[category].keywords.push(word);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const index = {
|
|
200
|
+
bundles,
|
|
201
|
+
metadata: {
|
|
202
|
+
builtAt: new Date().toISOString(),
|
|
203
|
+
docCount: files.length,
|
|
204
|
+
version: '1.0.0',
|
|
205
|
+
checksum: crypto
|
|
206
|
+
.createHash('sha256')
|
|
207
|
+
.update(JSON.stringify(bundles))
|
|
208
|
+
.digest('hex')
|
|
209
|
+
.slice(0, 12),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Persist to disk
|
|
214
|
+
try {
|
|
215
|
+
ensureDir(path.dirname(output));
|
|
216
|
+
fs.writeFileSync(output, JSON.stringify(index, null, 2));
|
|
217
|
+
} catch {
|
|
218
|
+
// Non-critical — index still returned in memory
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return index;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Context Router
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Score a single bundle against a set of query tokens.
|
|
230
|
+
*
|
|
231
|
+
* Counts how many query tokens match the bundle's keywords, then normalizes
|
|
232
|
+
* by bundle size to avoid large bundles always winning.
|
|
233
|
+
*
|
|
234
|
+
* @param {string[]} queryTokens - Lowercased query words
|
|
235
|
+
* @param {{ keywords: string[], docs: object[] }} bundle - A knowledge bundle
|
|
236
|
+
* @returns {number} Relevance score (higher is better)
|
|
237
|
+
*/
|
|
238
|
+
function scoreBundle(queryTokens, bundle) {
|
|
239
|
+
if (!bundle.keywords.length || !queryTokens.length) return 0;
|
|
240
|
+
|
|
241
|
+
let matches = 0;
|
|
242
|
+
for (const token of queryTokens) {
|
|
243
|
+
for (const keyword of bundle.keywords) {
|
|
244
|
+
if (keyword.includes(token) || token.includes(keyword)) {
|
|
245
|
+
matches++;
|
|
246
|
+
break; // Count each token at most once
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Normalize: raw matches / sqrt(bundle size) to balance precision vs. recall
|
|
252
|
+
const bundleSize = bundle.docs.length || 1;
|
|
253
|
+
return matches / Math.sqrt(bundleSize);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Route a natural-language query to the most relevant knowledge bundles.
|
|
258
|
+
*
|
|
259
|
+
* Replaces multiple MCP tool calls with a single pre-computed lookup.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} query - Natural-language query (e.g., "How do I fix Android build errors?")
|
|
262
|
+
* @param {string} [indexPath] - Path to the knowledge index JSON
|
|
263
|
+
* @param {number} [topN=3] - Number of top bundles to return
|
|
264
|
+
* @returns {{ query: string, results: object[] }} Top-N bundles with scores and doc references
|
|
265
|
+
*/
|
|
266
|
+
function routeQuery(query, indexPath, topN) {
|
|
267
|
+
const idxPath = indexPath || DEFAULT_INDEX_PATH;
|
|
268
|
+
const n = topN || 3;
|
|
269
|
+
|
|
270
|
+
// Load index
|
|
271
|
+
let index;
|
|
272
|
+
try {
|
|
273
|
+
index = JSON.parse(fs.readFileSync(idxPath, 'utf-8'));
|
|
274
|
+
} catch {
|
|
275
|
+
// Index doesn't exist — build it on the fly
|
|
276
|
+
index = buildKnowledgeIndex();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const queryTokens = query
|
|
280
|
+
.toLowerCase()
|
|
281
|
+
.split(/\s+/)
|
|
282
|
+
.filter((w) => w.length > 2);
|
|
283
|
+
|
|
284
|
+
const scored = Object.entries(index.bundles)
|
|
285
|
+
.map(([category, bundle]) => ({
|
|
286
|
+
category,
|
|
287
|
+
score: scoreBundle(queryTokens, bundle),
|
|
288
|
+
docs: bundle.docs,
|
|
289
|
+
}))
|
|
290
|
+
.filter((entry) => entry.score > 0)
|
|
291
|
+
.sort((a, b) => b.score - a.score)
|
|
292
|
+
.slice(0, n);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
query,
|
|
296
|
+
results: scored,
|
|
297
|
+
indexAge: index.metadata && index.metadata.builtAt,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Quality Scorer
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Score retrieval quality by comparing retrieved docs against expected topics.
|
|
307
|
+
*
|
|
308
|
+
* Uses a precision/recall-style metric:
|
|
309
|
+
* - Precision: what fraction of retrieved docs are relevant to expected topics?
|
|
310
|
+
* - Recall: what fraction of expected topics are covered by retrieved docs?
|
|
311
|
+
*
|
|
312
|
+
* @param {string} query - The original query
|
|
313
|
+
* @param {string[]} retrievedDocs - Filenames of retrieved docs
|
|
314
|
+
* @param {string[]} expectedTopics - Expected topic keywords to match against
|
|
315
|
+
* @returns {{ precision: number, recall: number, f1: number, query: string, timestamp: string }}
|
|
316
|
+
*/
|
|
317
|
+
function scoreRetrievalQuality(query, retrievedDocs, expectedTopics) {
|
|
318
|
+
if (!retrievedDocs.length || !expectedTopics.length) {
|
|
319
|
+
const result = {
|
|
320
|
+
query,
|
|
321
|
+
precision: 0,
|
|
322
|
+
recall: 0,
|
|
323
|
+
f1: 0,
|
|
324
|
+
retrievedCount: retrievedDocs.length,
|
|
325
|
+
expectedCount: expectedTopics.length,
|
|
326
|
+
timestamp: new Date().toISOString(),
|
|
327
|
+
};
|
|
328
|
+
logQualityResult(result);
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const normalizedDocs = retrievedDocs.map((d) => d.toLowerCase());
|
|
333
|
+
const normalizedTopics = expectedTopics.map((t) => t.toLowerCase());
|
|
334
|
+
|
|
335
|
+
// Precision: how many retrieved docs match at least one expected topic?
|
|
336
|
+
let relevantRetrieved = 0;
|
|
337
|
+
for (const doc of normalizedDocs) {
|
|
338
|
+
for (const topic of normalizedTopics) {
|
|
339
|
+
if (doc.includes(topic) || topic.includes(doc.replace('.md', ''))) {
|
|
340
|
+
relevantRetrieved++;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const precision = relevantRetrieved / normalizedDocs.length;
|
|
346
|
+
|
|
347
|
+
// Recall: how many expected topics are covered by at least one retrieved doc?
|
|
348
|
+
let topicsCovered = 0;
|
|
349
|
+
for (const topic of normalizedTopics) {
|
|
350
|
+
for (const doc of normalizedDocs) {
|
|
351
|
+
if (doc.includes(topic) || topic.includes(doc.replace('.md', ''))) {
|
|
352
|
+
topicsCovered++;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const recall = topicsCovered / normalizedTopics.length;
|
|
358
|
+
|
|
359
|
+
// F1 harmonic mean
|
|
360
|
+
const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
|
|
361
|
+
|
|
362
|
+
const result = {
|
|
363
|
+
query,
|
|
364
|
+
precision: Math.round(precision * 1000) / 1000,
|
|
365
|
+
recall: Math.round(recall * 1000) / 1000,
|
|
366
|
+
f1: Math.round(f1 * 1000) / 1000,
|
|
367
|
+
retrievedCount: retrievedDocs.length,
|
|
368
|
+
expectedCount: expectedTopics.length,
|
|
369
|
+
timestamp: new Date().toISOString(),
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
logQualityResult(result);
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Append a quality result to the JSONL quality log.
|
|
378
|
+
*
|
|
379
|
+
* @param {object} result - Quality score result object
|
|
380
|
+
* @param {string} [logPath] - Path to the quality log file
|
|
381
|
+
*/
|
|
382
|
+
function logQualityResult(result, logPath) {
|
|
383
|
+
const log = logPath || DEFAULT_QUALITY_LOG_PATH;
|
|
384
|
+
const entry = { ...result };
|
|
385
|
+
|
|
386
|
+
// Ensure timestamp is always present
|
|
387
|
+
if (!entry.timestamp) {
|
|
388
|
+
entry.timestamp = new Date().toISOString();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
ensureDir(path.dirname(log));
|
|
393
|
+
fs.appendFileSync(log, JSON.stringify(entry) + '\n');
|
|
394
|
+
} catch {
|
|
395
|
+
// Non-critical — scoring still works without persistence
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Prompt Registry
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Load the prompt registry from disk.
|
|
405
|
+
*
|
|
406
|
+
* @param {string} [registryPath] - Path to the registry JSON
|
|
407
|
+
* @returns {object} Map of prompt name → { template, metadata }
|
|
408
|
+
*/
|
|
409
|
+
function loadRegistry(registryPath) {
|
|
410
|
+
const reg = registryPath || DEFAULT_REGISTRY_PATH;
|
|
411
|
+
try {
|
|
412
|
+
if (fs.existsSync(reg)) {
|
|
413
|
+
return JSON.parse(fs.readFileSync(reg, 'utf-8'));
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
// Corrupted file — start fresh
|
|
417
|
+
}
|
|
418
|
+
return {};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Save the prompt registry to disk.
|
|
423
|
+
*
|
|
424
|
+
* @param {object} registry - The full registry object
|
|
425
|
+
* @param {string} [registryPath] - Path to the registry JSON
|
|
426
|
+
*/
|
|
427
|
+
function saveRegistry(registry, registryPath) {
|
|
428
|
+
const reg = registryPath || DEFAULT_REGISTRY_PATH;
|
|
429
|
+
try {
|
|
430
|
+
ensureDir(path.dirname(reg));
|
|
431
|
+
fs.writeFileSync(reg, JSON.stringify(registry, null, 2));
|
|
432
|
+
} catch {
|
|
433
|
+
// Non-critical — registry still works in memory
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Register a prompt template with version and model compatibility metadata.
|
|
439
|
+
*
|
|
440
|
+
* @param {string} name - Unique prompt name (e.g., "code-review-system")
|
|
441
|
+
* @param {string} template - The prompt template string
|
|
442
|
+
* @param {{ version: string, models: string[], category: string }} metadata - Prompt metadata
|
|
443
|
+
* @param {string} [registryPath] - Path to the registry JSON
|
|
444
|
+
* @returns {{ name: string, registered: boolean }} Registration result
|
|
445
|
+
*/
|
|
446
|
+
function registerPrompt(name, template, metadata, registryPath) {
|
|
447
|
+
const registry = loadRegistry(registryPath);
|
|
448
|
+
|
|
449
|
+
// Support both metadata.models (array) and metadata.model (single string)
|
|
450
|
+
let models = [];
|
|
451
|
+
if (metadata && metadata.models) {
|
|
452
|
+
models = metadata.models;
|
|
453
|
+
} else if (metadata && metadata.model) {
|
|
454
|
+
models = [metadata.model];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
registry[name] = {
|
|
458
|
+
template,
|
|
459
|
+
metadata: {
|
|
460
|
+
version: (metadata && metadata.version) || '1.0.0',
|
|
461
|
+
models,
|
|
462
|
+
category: (metadata && metadata.category) || 'general',
|
|
463
|
+
lastUpdated: new Date().toISOString(),
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
saveRegistry(registry, registryPath);
|
|
468
|
+
return { name, registered: true };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Retrieve a registered prompt, optionally filtering by model compatibility.
|
|
473
|
+
*
|
|
474
|
+
* @param {string} name - Prompt name to look up
|
|
475
|
+
* @param {string} [modelId] - Optional model ID to check compatibility
|
|
476
|
+
* @param {string} [registryPath] - Path to the registry JSON
|
|
477
|
+
* @returns {{ name: string, template: string, metadata: object, compatible: boolean }|null}
|
|
478
|
+
*/
|
|
479
|
+
function getPrompt(name, modelId, registryPath) {
|
|
480
|
+
const registry = loadRegistry(registryPath);
|
|
481
|
+
const entry = registry[name];
|
|
482
|
+
|
|
483
|
+
if (!entry) return null;
|
|
484
|
+
|
|
485
|
+
const compatible =
|
|
486
|
+
!modelId || !entry.metadata.models.length || entry.metadata.models.includes(modelId);
|
|
487
|
+
|
|
488
|
+
// If a specific model was requested and it's not compatible, return null
|
|
489
|
+
if (modelId && entry.metadata.models.length > 0 && !entry.metadata.models.includes(modelId)) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
name,
|
|
495
|
+
template: entry.template,
|
|
496
|
+
metadata: entry.metadata,
|
|
497
|
+
compatible,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* List all registered prompts with their metadata.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} [registryPath] - Path to the registry JSON
|
|
505
|
+
* @returns {{ name: string, metadata: object }[]} Array of prompt entries
|
|
506
|
+
*/
|
|
507
|
+
function listPrompts(registryPath) {
|
|
508
|
+
const registry = loadRegistry(registryPath);
|
|
509
|
+
|
|
510
|
+
return Object.entries(registry).map(([name, entry]) => ({
|
|
511
|
+
name,
|
|
512
|
+
metadata: entry.metadata,
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// Exports
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
module.exports = {
|
|
521
|
+
// Knowledge Bundle Builder
|
|
522
|
+
buildKnowledgeIndex,
|
|
523
|
+
categorizeDoc,
|
|
524
|
+
extractDocSummary,
|
|
525
|
+
|
|
526
|
+
// Context Router
|
|
527
|
+
routeQuery,
|
|
528
|
+
scoreBundle,
|
|
529
|
+
|
|
530
|
+
// Quality Scorer
|
|
531
|
+
scoreRetrievalQuality,
|
|
532
|
+
logQualityResult,
|
|
533
|
+
|
|
534
|
+
// Prompt Registry
|
|
535
|
+
registerPrompt,
|
|
536
|
+
getPrompt,
|
|
537
|
+
listPrompts,
|
|
538
|
+
|
|
539
|
+
// MCP Consolidation Manifest
|
|
540
|
+
TOOL_CONSOLIDATION,
|
|
541
|
+
|
|
542
|
+
// Constants (for testing / external use)
|
|
543
|
+
CATEGORY_RULES,
|
|
544
|
+
DEFAULT_INDEX_PATH,
|
|
545
|
+
DEFAULT_QUALITY_LOG_PATH,
|
|
546
|
+
DEFAULT_REGISTRY_PATH,
|
|
547
|
+
};
|