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,838 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* RLHF Feedback Loop (local-first)
|
|
4
|
+
*
|
|
5
|
+
* Pipeline:
|
|
6
|
+
* thumbs up/down -> resolve action -> validate memory -> append logs
|
|
7
|
+
* -> compute analytics -> generate prevention rules
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const {
|
|
13
|
+
resolveFeedbackAction,
|
|
14
|
+
prepareForStorage,
|
|
15
|
+
parseTimestamp,
|
|
16
|
+
} = require('./feedback-schema');
|
|
17
|
+
const {
|
|
18
|
+
buildRubricEvaluation,
|
|
19
|
+
} = require('./rubric-engine');
|
|
20
|
+
const { recordAction, attributeFeedback } = require('./feedback-attribution');
|
|
21
|
+
|
|
22
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
23
|
+
const DEFAULT_FEEDBACK_DIR = path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
|
|
24
|
+
|
|
25
|
+
// ML sequence tracking constants (ML-03)
|
|
26
|
+
const SEQUENCE_WINDOW = 10;
|
|
27
|
+
const DOMAIN_CATEGORIES = [
|
|
28
|
+
'testing', 'security', 'performance', 'ui-components', 'api-integration',
|
|
29
|
+
'git-workflow', 'documentation', 'debugging', 'architecture', 'data-modeling',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function getFeedbackPaths() {
|
|
33
|
+
const feedbackDir = process.env.RLHF_FEEDBACK_DIR || DEFAULT_FEEDBACK_DIR;
|
|
34
|
+
return {
|
|
35
|
+
FEEDBACK_DIR: feedbackDir,
|
|
36
|
+
FEEDBACK_LOG_PATH: path.join(feedbackDir, 'feedback-log.jsonl'),
|
|
37
|
+
MEMORY_LOG_PATH: path.join(feedbackDir, 'memory-log.jsonl'),
|
|
38
|
+
SUMMARY_PATH: path.join(feedbackDir, 'feedback-summary.json'),
|
|
39
|
+
PREVENTION_RULES_PATH: path.join(feedbackDir, 'prevention-rules.md'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getContextFsModule() {
|
|
44
|
+
try {
|
|
45
|
+
return require('./contextfs');
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getVectorStoreModule() {
|
|
52
|
+
try {
|
|
53
|
+
return require('./vector-store');
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getSelfAuditModule() {
|
|
60
|
+
try {
|
|
61
|
+
return require('./rlaif-self-audit');
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureDir(dirPath) {
|
|
68
|
+
if (!fs.existsSync(dirPath)) {
|
|
69
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function appendJSONL(filePath, record) {
|
|
74
|
+
ensureDir(path.dirname(filePath));
|
|
75
|
+
fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readJSONL(filePath) {
|
|
79
|
+
if (!fs.existsSync(filePath)) return [];
|
|
80
|
+
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
81
|
+
if (!raw) return [];
|
|
82
|
+
return raw
|
|
83
|
+
.split('\n')
|
|
84
|
+
.map((line) => {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(line);
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeSignal(signal) {
|
|
95
|
+
const value = String(signal || '').trim().toLowerCase();
|
|
96
|
+
if (['up', 'thumbsup', 'thumbs-up', 'positive', 'good'].includes(value)) return 'positive';
|
|
97
|
+
if (['down', 'thumbsdown', 'thumbs-down', 'negative', 'bad'].includes(value)) return 'negative';
|
|
98
|
+
if (value === 'thumbs_up') return 'positive';
|
|
99
|
+
if (value === 'thumbs_down') return 'negative';
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseOptionalObject(input, name) {
|
|
104
|
+
if (input == null) return {};
|
|
105
|
+
if (typeof input === 'object' && !Array.isArray(input)) return input;
|
|
106
|
+
if (typeof input === 'string') {
|
|
107
|
+
const trimmed = input.trim();
|
|
108
|
+
if (!trimmed) return {};
|
|
109
|
+
const parsed = JSON.parse(trimmed);
|
|
110
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
111
|
+
throw new Error(`${name} must be an object`);
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`${name} must be object or JSON string`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function loadSummary() {
|
|
119
|
+
const { SUMMARY_PATH } = getFeedbackPaths();
|
|
120
|
+
if (!fs.existsSync(SUMMARY_PATH)) {
|
|
121
|
+
return {
|
|
122
|
+
total: 0,
|
|
123
|
+
positive: 0,
|
|
124
|
+
negative: 0,
|
|
125
|
+
accepted: 0,
|
|
126
|
+
rejected: 0,
|
|
127
|
+
lastUpdated: null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return JSON.parse(fs.readFileSync(SUMMARY_PATH, 'utf-8'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function saveSummary(summary) {
|
|
134
|
+
const { SUMMARY_PATH } = getFeedbackPaths();
|
|
135
|
+
ensureDir(path.dirname(SUMMARY_PATH));
|
|
136
|
+
fs.writeFileSync(SUMMARY_PATH, `${JSON.stringify(summary, null, 2)}\n`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================
|
|
140
|
+
// ML Side-Effect Helpers — Sequence Tracking (ML-03) and
|
|
141
|
+
// Diversity Tracking (ML-04). Inline per Subway architecture.
|
|
142
|
+
// ============================================================
|
|
143
|
+
|
|
144
|
+
function inferDomain(tags, context) {
|
|
145
|
+
const tagSet = new Set((tags || []).map((t) => t.toLowerCase()));
|
|
146
|
+
const ctx = (context || '').toLowerCase();
|
|
147
|
+
if (tagSet.has('test') || tagSet.has('testing') || ctx.includes('test')) return 'testing';
|
|
148
|
+
if (tagSet.has('security') || ctx.includes('secret')) return 'security';
|
|
149
|
+
if (tagSet.has('perf') || tagSet.has('performance') || ctx.includes('performance')) return 'performance';
|
|
150
|
+
if (tagSet.has('ui') || tagSet.has('component') || ctx.includes('component')) return 'ui-components';
|
|
151
|
+
if (tagSet.has('api') || tagSet.has('endpoint') || ctx.includes('endpoint')) return 'api-integration';
|
|
152
|
+
if (tagSet.has('git') || tagSet.has('commit') || ctx.includes('commit')) return 'git-workflow';
|
|
153
|
+
if (tagSet.has('doc') || tagSet.has('readme') || ctx.includes('readme')) return 'documentation';
|
|
154
|
+
if (tagSet.has('debug') || tagSet.has('debugging') || ctx.includes('error')) return 'debugging';
|
|
155
|
+
if (tagSet.has('arch') || tagSet.has('architecture') || ctx.includes('design')) return 'architecture';
|
|
156
|
+
if (tagSet.has('data') || tagSet.has('schema') || ctx.includes('schema')) return 'data-modeling';
|
|
157
|
+
return 'general';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Infer granular outcome category from signal + context.
|
|
162
|
+
* Satisfies QUAL-03 — beyond binary up/down.
|
|
163
|
+
* @param {string} signal - 'positive' or 'negative'
|
|
164
|
+
* @param {string} context - feedback context string
|
|
165
|
+
* @returns {string} granular outcome category
|
|
166
|
+
*/
|
|
167
|
+
function inferOutcome(signal, context) {
|
|
168
|
+
const cl = (context || '').toLowerCase();
|
|
169
|
+
if (signal === 'positive') {
|
|
170
|
+
if (cl.includes('first try') || cl.includes('immediately') || cl.includes('right away')) return 'quick-success';
|
|
171
|
+
if (cl.includes('thorough') || cl.includes('comprehensive') || cl.includes('in-depth')) return 'deep-success';
|
|
172
|
+
if (cl.includes('creative') || cl.includes('novel') || cl.includes('elegant')) return 'creative-success';
|
|
173
|
+
if (cl.includes('partial') || cl.includes('mostly') || cl.includes('some issues')) return 'partial-success';
|
|
174
|
+
return 'standard-success';
|
|
175
|
+
} else {
|
|
176
|
+
if (cl.includes('wrong') || cl.includes('incorrect') || cl.includes('factual')) return 'factual-error';
|
|
177
|
+
if (cl.includes('shallow') || cl.includes('surface') || cl.includes('superficial')) return 'insufficient-depth';
|
|
178
|
+
if (cl.includes('slow') || cl.includes('took too long') || cl.includes('inefficient')) return 'efficiency-issue';
|
|
179
|
+
if (cl.includes('assumption') || cl.includes('guessed') || cl.includes('assumed')) return 'false-assumption';
|
|
180
|
+
if (cl.includes('partial') || cl.includes('incomplete') || cl.includes('missing')) return 'incomplete';
|
|
181
|
+
return 'standard-failure';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Enrich feedbackEvent with richContext metadata.
|
|
187
|
+
* Satisfies QUAL-02 — domain, filePaths, errorType, outcomeCategory.
|
|
188
|
+
* Non-throwing: returns original event on any error.
|
|
189
|
+
* @param {object} feedbackEvent - base feedback event
|
|
190
|
+
* @param {object} params - original captureFeedback params
|
|
191
|
+
* @returns {object} enriched feedbackEvent
|
|
192
|
+
*/
|
|
193
|
+
function enrichFeedbackContext(feedbackEvent, params) {
|
|
194
|
+
try {
|
|
195
|
+
const domain = inferDomain(feedbackEvent.tags, feedbackEvent.context);
|
|
196
|
+
const outcomeCategory = inferOutcome(feedbackEvent.signal, feedbackEvent.context);
|
|
197
|
+
const filePaths = Array.isArray(params.filePaths)
|
|
198
|
+
? params.filePaths
|
|
199
|
+
: typeof params.filePaths === 'string' && params.filePaths.trim()
|
|
200
|
+
? params.filePaths.split(',').map((f) => f.trim()).filter(Boolean)
|
|
201
|
+
: [];
|
|
202
|
+
const errorType = params.errorType || null;
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
...feedbackEvent,
|
|
206
|
+
richContext: {
|
|
207
|
+
domain,
|
|
208
|
+
filePaths,
|
|
209
|
+
errorType,
|
|
210
|
+
outcomeCategory,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
} catch (_err) {
|
|
214
|
+
return feedbackEvent;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function calculateTrend(rewards) {
|
|
219
|
+
if (rewards.length < 2) return 0;
|
|
220
|
+
const recent = rewards.slice(-3);
|
|
221
|
+
return recent.reduce((a, b) => a + b, 0) / recent.length;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function calculateTimeGaps(sequence) {
|
|
225
|
+
const gaps = [];
|
|
226
|
+
for (let i = 1; i < sequence.length; i++) {
|
|
227
|
+
const prev = parseTimestamp(sequence[i - 1].timestamp);
|
|
228
|
+
const curr = parseTimestamp(sequence[i].timestamp);
|
|
229
|
+
if (prev && curr) {
|
|
230
|
+
gaps.push((curr - prev) / 1000 / 60); // minutes
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return gaps;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function extractActionPatterns(sequence) {
|
|
237
|
+
const patterns = {};
|
|
238
|
+
sequence.forEach((f) => {
|
|
239
|
+
(f.tags || []).forEach((tag) => {
|
|
240
|
+
if (!patterns[tag]) patterns[tag] = { positive: 0, negative: 0 };
|
|
241
|
+
if (f.signal === 'positive') patterns[tag].positive++;
|
|
242
|
+
else patterns[tag].negative++;
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
return patterns;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildSequenceFeatures(recentEntries, currentEntry) {
|
|
249
|
+
const sequence = [...recentEntries, currentEntry];
|
|
250
|
+
return {
|
|
251
|
+
rewardSequence: sequence.map((f) => (f.signal === 'positive' ? 1 : -1)),
|
|
252
|
+
tagFrequency: sequence.reduce((acc, f) => {
|
|
253
|
+
(f.tags || []).forEach((tag) => {
|
|
254
|
+
acc[tag] = (acc[tag] || 0) + 1;
|
|
255
|
+
});
|
|
256
|
+
return acc;
|
|
257
|
+
}, {}),
|
|
258
|
+
recentTrend: calculateTrend(sequence.slice(-5).map((f) => (f.signal === 'positive' ? 1 : -1))),
|
|
259
|
+
timeGaps: calculateTimeGaps(sequence),
|
|
260
|
+
actionPatterns: extractActionPatterns(sequence),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function appendSequence(feedbackEvent, paths) {
|
|
265
|
+
const sequencePath = path.join(paths.FEEDBACK_DIR, 'feedback-sequences.jsonl');
|
|
266
|
+
const recent = readJSONL(paths.FEEDBACK_LOG_PATH).slice(-SEQUENCE_WINDOW);
|
|
267
|
+
const features = buildSequenceFeatures(recent, feedbackEvent);
|
|
268
|
+
const entry = {
|
|
269
|
+
id: `seq_${Date.now()}`,
|
|
270
|
+
timestamp: new Date().toISOString(),
|
|
271
|
+
targetReward: feedbackEvent.signal === 'positive' ? 1 : -1,
|
|
272
|
+
targetTags: feedbackEvent.tags,
|
|
273
|
+
features,
|
|
274
|
+
label: feedbackEvent.signal === 'positive' ? 'positive' : 'negative',
|
|
275
|
+
};
|
|
276
|
+
appendJSONL(sequencePath, entry);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function updateDiversityTracking(feedbackEvent, paths) {
|
|
280
|
+
const diversityPath = path.join(paths.FEEDBACK_DIR, 'diversity-tracking.json');
|
|
281
|
+
let diversity = { domains: {}, lastUpdated: null, diversityScore: 0 };
|
|
282
|
+
if (fs.existsSync(diversityPath)) {
|
|
283
|
+
try {
|
|
284
|
+
diversity = JSON.parse(fs.readFileSync(diversityPath, 'utf-8'));
|
|
285
|
+
} catch {
|
|
286
|
+
// start fresh on parse error
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const domain = inferDomain(feedbackEvent.tags, feedbackEvent.context);
|
|
291
|
+
if (!diversity.domains[domain]) {
|
|
292
|
+
diversity.domains[domain] = { count: 0, positive: 0, negative: 0, lastSeen: null };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
diversity.domains[domain].count++;
|
|
296
|
+
diversity.domains[domain].lastSeen = feedbackEvent.timestamp;
|
|
297
|
+
if (feedbackEvent.signal === 'positive') diversity.domains[domain].positive++;
|
|
298
|
+
else diversity.domains[domain].negative++;
|
|
299
|
+
|
|
300
|
+
const totalFeedback = Object.values(diversity.domains).reduce((s, d) => s + d.count, 0);
|
|
301
|
+
const domainCount = Object.keys(diversity.domains).length;
|
|
302
|
+
const idealPerDomain = totalFeedback / DOMAIN_CATEGORIES.length;
|
|
303
|
+
const variance = Object.values(diversity.domains).reduce((s, d) => {
|
|
304
|
+
return s + Math.pow(d.count - idealPerDomain, 2);
|
|
305
|
+
}, 0) / Math.max(domainCount, 1);
|
|
306
|
+
|
|
307
|
+
diversity.diversityScore = Math.max(0, 100 - Math.sqrt(variance) * 10).toFixed(1);
|
|
308
|
+
diversity.lastUpdated = new Date().toISOString();
|
|
309
|
+
diversity.recommendation = Number(diversity.diversityScore) < 50
|
|
310
|
+
? `Low diversity (${diversity.diversityScore}%). Try feedback in: ${DOMAIN_CATEGORIES.filter((d) => !diversity.domains[d]).join(', ')}`
|
|
311
|
+
: `Good diversity (${diversity.diversityScore}%)`;
|
|
312
|
+
|
|
313
|
+
fs.writeFileSync(diversityPath, JSON.stringify(diversity, null, 2) + '\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function captureFeedback(params) {
|
|
317
|
+
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } = getFeedbackPaths();
|
|
318
|
+
const signal = normalizeSignal(params.signal);
|
|
319
|
+
if (!signal) {
|
|
320
|
+
return {
|
|
321
|
+
accepted: false,
|
|
322
|
+
reason: `Invalid signal "${params.signal}". Use up/down or positive/negative.`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const tags = Array.isArray(params.tags)
|
|
327
|
+
? params.tags
|
|
328
|
+
: String(params.tags || '')
|
|
329
|
+
.split(',')
|
|
330
|
+
.map((t) => t.trim())
|
|
331
|
+
.filter(Boolean);
|
|
332
|
+
|
|
333
|
+
let rubricEvaluation = null;
|
|
334
|
+
try {
|
|
335
|
+
if (params.rubricScores != null || params.guardrails != null) {
|
|
336
|
+
rubricEvaluation = buildRubricEvaluation({
|
|
337
|
+
rubricScores: params.rubricScores,
|
|
338
|
+
guardrails: parseOptionalObject(params.guardrails, 'guardrails'),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
return {
|
|
343
|
+
accepted: false,
|
|
344
|
+
reason: `Invalid rubric payload: ${err.message}`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const action = resolveFeedbackAction({
|
|
349
|
+
signal,
|
|
350
|
+
context: params.context || '',
|
|
351
|
+
whatWentWrong: params.whatWentWrong,
|
|
352
|
+
whatToChange: params.whatToChange,
|
|
353
|
+
whatWorked: params.whatWorked,
|
|
354
|
+
tags,
|
|
355
|
+
rubricEvaluation,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const now = new Date().toISOString();
|
|
359
|
+
const rawFeedbackEvent = {
|
|
360
|
+
id: `fb_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
361
|
+
signal,
|
|
362
|
+
context: params.context || '',
|
|
363
|
+
whatWentWrong: params.whatWentWrong || null,
|
|
364
|
+
whatToChange: params.whatToChange || null,
|
|
365
|
+
whatWorked: params.whatWorked || null,
|
|
366
|
+
tags,
|
|
367
|
+
skill: params.skill || null,
|
|
368
|
+
rubric: rubricEvaluation
|
|
369
|
+
? {
|
|
370
|
+
rubricId: rubricEvaluation.rubricId,
|
|
371
|
+
weightedScore: rubricEvaluation.weightedScore,
|
|
372
|
+
failingCriteria: rubricEvaluation.failingCriteria,
|
|
373
|
+
failingGuardrails: rubricEvaluation.failingGuardrails,
|
|
374
|
+
judgeDisagreements: rubricEvaluation.judgeDisagreements,
|
|
375
|
+
promotionEligible: rubricEvaluation.promotionEligible,
|
|
376
|
+
}
|
|
377
|
+
: null,
|
|
378
|
+
actionType: action.type,
|
|
379
|
+
actionReason: action.reason || null,
|
|
380
|
+
timestamp: now,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Rich context enrichment (QUAL-02, QUAL-03) — non-blocking
|
|
384
|
+
const feedbackEvent = enrichFeedbackContext(rawFeedbackEvent, params);
|
|
385
|
+
|
|
386
|
+
const summary = loadSummary();
|
|
387
|
+
summary.total += 1;
|
|
388
|
+
summary[signal] += 1;
|
|
389
|
+
|
|
390
|
+
if (action.type === 'no-action') {
|
|
391
|
+
summary.rejected += 1;
|
|
392
|
+
summary.lastUpdated = now;
|
|
393
|
+
saveSummary(summary);
|
|
394
|
+
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
395
|
+
return {
|
|
396
|
+
accepted: false,
|
|
397
|
+
reason: action.reason,
|
|
398
|
+
feedbackEvent,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const prepared = prepareForStorage(action.memory);
|
|
403
|
+
if (!prepared.ok) {
|
|
404
|
+
summary.rejected += 1;
|
|
405
|
+
summary.lastUpdated = now;
|
|
406
|
+
saveSummary(summary);
|
|
407
|
+
appendJSONL(FEEDBACK_LOG_PATH, {
|
|
408
|
+
...feedbackEvent,
|
|
409
|
+
validationIssues: prepared.issues,
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
accepted: false,
|
|
413
|
+
reason: `Schema validation failed: ${prepared.issues.join('; ')}`,
|
|
414
|
+
feedbackEvent,
|
|
415
|
+
issues: prepared.issues,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const memoryRecord = {
|
|
420
|
+
id: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
421
|
+
...prepared.memory,
|
|
422
|
+
sourceFeedbackId: feedbackEvent.id,
|
|
423
|
+
timestamp: now,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
427
|
+
appendJSONL(MEMORY_LOG_PATH, memoryRecord);
|
|
428
|
+
|
|
429
|
+
const contextFs = getContextFsModule();
|
|
430
|
+
if (contextFs && typeof contextFs.registerFeedback === 'function') {
|
|
431
|
+
try {
|
|
432
|
+
contextFs.registerFeedback(feedbackEvent, memoryRecord);
|
|
433
|
+
} catch {
|
|
434
|
+
// Non-critical; feedback remains in primary logs
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ML side-effects: sequence tracking and diversity (non-blocking — primary write already succeeded)
|
|
439
|
+
const mlPaths = getFeedbackPaths();
|
|
440
|
+
try {
|
|
441
|
+
appendSequence(feedbackEvent, mlPaths);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
// Sequence tracking failure is non-critical
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
updateDiversityTracking(feedbackEvent, mlPaths);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
// Diversity tracking failure is non-critical
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Vector storage side-effect (non-blocking — primary write already succeeded)
|
|
452
|
+
const vectorStore = getVectorStoreModule();
|
|
453
|
+
if (vectorStore) {
|
|
454
|
+
vectorStore.upsertFeedback(feedbackEvent).catch(() => {
|
|
455
|
+
// Non-critical; primary feedback log is the source of truth
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// RLAIF self-audit side-effect (non-blocking — 4th enrichment layer)
|
|
460
|
+
try {
|
|
461
|
+
const sam = getSelfAuditModule();
|
|
462
|
+
if (sam) sam.selfAuditAndLog(feedbackEvent, mlPaths);
|
|
463
|
+
} catch (_err) { /* non-critical */ }
|
|
464
|
+
|
|
465
|
+
// Attribution side-effects — fire-and-forget, never throw
|
|
466
|
+
try {
|
|
467
|
+
const toolName = feedbackEvent.toolName || feedbackEvent.tool_name || 'unknown';
|
|
468
|
+
const toolInput = feedbackEvent.context || feedbackEvent.input || '';
|
|
469
|
+
recordAction(toolName, toolInput);
|
|
470
|
+
if (feedbackEvent.signal === 'negative') {
|
|
471
|
+
attributeFeedback('negative', feedbackEvent.context || '');
|
|
472
|
+
} else if (feedbackEvent.signal === 'positive') {
|
|
473
|
+
attributeFeedback('positive', feedbackEvent.context || '');
|
|
474
|
+
}
|
|
475
|
+
} catch (e) {
|
|
476
|
+
// attribution is non-blocking
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
summary.accepted += 1;
|
|
480
|
+
summary.lastUpdated = now;
|
|
481
|
+
saveSummary(summary);
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
accepted: true,
|
|
485
|
+
feedbackEvent,
|
|
486
|
+
memoryRecord,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function analyzeFeedback(logPath) {
|
|
491
|
+
const { FEEDBACK_LOG_PATH } = getFeedbackPaths();
|
|
492
|
+
const entries = readJSONL(logPath || FEEDBACK_LOG_PATH);
|
|
493
|
+
const skills = {};
|
|
494
|
+
const tags = {};
|
|
495
|
+
const rubricCriteria = {};
|
|
496
|
+
let rubricSamples = 0;
|
|
497
|
+
let blockedPromotions = 0;
|
|
498
|
+
|
|
499
|
+
let totalPositive = 0;
|
|
500
|
+
let totalNegative = 0;
|
|
501
|
+
|
|
502
|
+
for (const entry of entries) {
|
|
503
|
+
if (entry.signal === 'positive') totalPositive++;
|
|
504
|
+
if (entry.signal === 'negative') totalNegative++;
|
|
505
|
+
|
|
506
|
+
if (entry.skill) {
|
|
507
|
+
if (!skills[entry.skill]) skills[entry.skill] = { positive: 0, negative: 0, total: 0 };
|
|
508
|
+
skills[entry.skill][entry.signal] += 1;
|
|
509
|
+
skills[entry.skill].total += 1;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const tag of entry.tags || []) {
|
|
513
|
+
if (!tags[tag]) tags[tag] = { positive: 0, negative: 0, total: 0 };
|
|
514
|
+
tags[tag][entry.signal] += 1;
|
|
515
|
+
tags[tag].total += 1;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (entry.actionType === 'no-action' && typeof entry.actionReason === 'string' && entry.actionReason.includes('Rubric gate')) {
|
|
519
|
+
blockedPromotions += 1;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (entry.rubric && entry.rubric.weightedScore != null) {
|
|
523
|
+
rubricSamples += 1;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (entry.rubric && Array.isArray(entry.rubric.failingCriteria)) {
|
|
527
|
+
for (const criterion of entry.rubric.failingCriteria) {
|
|
528
|
+
if (!rubricCriteria[criterion]) rubricCriteria[criterion] = { failures: 0 };
|
|
529
|
+
rubricCriteria[criterion].failures += 1;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const total = totalPositive + totalNegative;
|
|
535
|
+
const approvalRate = total > 0 ? Math.round((totalPositive / total) * 1000) / 1000 : 0;
|
|
536
|
+
const recent = entries.slice(-20);
|
|
537
|
+
const recentPos = recent.filter((e) => e.signal === 'positive').length;
|
|
538
|
+
const recentRate = recent.length > 0 ? Math.round((recentPos / recent.length) * 1000) / 1000 : 0;
|
|
539
|
+
|
|
540
|
+
const recommendations = [];
|
|
541
|
+
|
|
542
|
+
for (const [skill, stat] of Object.entries(skills)) {
|
|
543
|
+
const negRate = stat.total > 0 ? stat.negative / stat.total : 0;
|
|
544
|
+
if (stat.total >= 3 && negRate >= 0.5) {
|
|
545
|
+
recommendations.push(`IMPROVE skill '${skill}' (${stat.negative}/${stat.total} negative)`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for (const [tag, stat] of Object.entries(tags)) {
|
|
550
|
+
const posRate = stat.total > 0 ? stat.positive / stat.total : 0;
|
|
551
|
+
if (stat.total >= 3 && posRate >= 0.8) {
|
|
552
|
+
recommendations.push(`REUSE pattern '${tag}' (${stat.positive}/${stat.total} positive)`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (recent.length >= 10 && recentRate < approvalRate - 0.1) {
|
|
557
|
+
recommendations.push('DECLINING trend in last 20 signals; tighten verification before response.');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
total,
|
|
562
|
+
totalPositive,
|
|
563
|
+
totalNegative,
|
|
564
|
+
approvalRate,
|
|
565
|
+
recentRate,
|
|
566
|
+
skills,
|
|
567
|
+
tags,
|
|
568
|
+
rubric: {
|
|
569
|
+
samples: rubricSamples,
|
|
570
|
+
blockedPromotions,
|
|
571
|
+
failingCriteria: rubricCriteria,
|
|
572
|
+
},
|
|
573
|
+
recommendations,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function buildPreventionRules(minOccurrences = 2) {
|
|
578
|
+
const { MEMORY_LOG_PATH } = getFeedbackPaths();
|
|
579
|
+
const memories = readJSONL(MEMORY_LOG_PATH).filter((m) => m.category === 'error');
|
|
580
|
+
if (memories.length === 0) {
|
|
581
|
+
return '# Prevention Rules\n\nNo mistake memories recorded yet.';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const buckets = {};
|
|
585
|
+
const rubricBuckets = {};
|
|
586
|
+
for (const m of memories) {
|
|
587
|
+
const key = (m.tags || []).find((t) => !['feedback', 'negative', 'positive'].includes(t)) || 'general';
|
|
588
|
+
if (!buckets[key]) buckets[key] = [];
|
|
589
|
+
buckets[key].push(m);
|
|
590
|
+
|
|
591
|
+
const failed = m.rubricSummary && Array.isArray(m.rubricSummary.failingCriteria)
|
|
592
|
+
? m.rubricSummary.failingCriteria
|
|
593
|
+
: [];
|
|
594
|
+
failed.forEach((criterion) => {
|
|
595
|
+
if (!rubricBuckets[criterion]) rubricBuckets[criterion] = [];
|
|
596
|
+
rubricBuckets[criterion].push(m);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const lines = ['# Prevention Rules', '', 'Generated from negative feedback memories.'];
|
|
601
|
+
|
|
602
|
+
Object.entries(buckets)
|
|
603
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
604
|
+
.forEach(([domain, items]) => {
|
|
605
|
+
if (items.length < minOccurrences) return;
|
|
606
|
+
const latest = items[items.length - 1];
|
|
607
|
+
const avoid = (latest.content || '').split('\n').find((l) => l.toLowerCase().startsWith('how to avoid:')) || 'How to avoid: Investigate and prevent recurrence';
|
|
608
|
+
lines.push('');
|
|
609
|
+
lines.push(`## ${domain}`);
|
|
610
|
+
lines.push(`- Recurrence count: ${items.length}`);
|
|
611
|
+
lines.push(`- Rule: ${avoid.replace(/^How to avoid:\s*/i, '')}`);
|
|
612
|
+
lines.push(`- Latest mistake: ${latest.title}`);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const rubricEntries = Object.entries(rubricBuckets).sort((a, b) => b[1].length - a[1].length);
|
|
616
|
+
if (rubricEntries.length > 0) {
|
|
617
|
+
lines.push('');
|
|
618
|
+
lines.push('## Rubric Failure Dimensions');
|
|
619
|
+
rubricEntries.forEach(([criterion, items]) => {
|
|
620
|
+
if (items.length < minOccurrences) return;
|
|
621
|
+
lines.push(`- ${criterion}: ${items.length} failures`);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (lines.length === 3) {
|
|
626
|
+
lines.push('');
|
|
627
|
+
lines.push(`No domain has reached the threshold (${minOccurrences}) yet.`);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return lines.join('\n');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function writePreventionRules(filePath, minOccurrences = 2) {
|
|
634
|
+
const { PREVENTION_RULES_PATH } = getFeedbackPaths();
|
|
635
|
+
const outPath = filePath || PREVENTION_RULES_PATH;
|
|
636
|
+
const markdown = buildPreventionRules(minOccurrences);
|
|
637
|
+
ensureDir(path.dirname(outPath));
|
|
638
|
+
fs.writeFileSync(outPath, `${markdown}\n`);
|
|
639
|
+
|
|
640
|
+
const contextFs = getContextFsModule();
|
|
641
|
+
if (contextFs && typeof contextFs.registerPreventionRules === 'function') {
|
|
642
|
+
try {
|
|
643
|
+
contextFs.registerPreventionRules(markdown, { minOccurrences, outputPath: outPath });
|
|
644
|
+
} catch {
|
|
645
|
+
// Non-critical
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return { path: outPath, markdown };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function feedbackSummary(recentN = 20) {
|
|
652
|
+
const { FEEDBACK_LOG_PATH } = getFeedbackPaths();
|
|
653
|
+
const entries = readJSONL(FEEDBACK_LOG_PATH);
|
|
654
|
+
if (entries.length === 0) {
|
|
655
|
+
return '## Feedback Summary\nNo feedback recorded yet.';
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const recent = entries.slice(-recentN);
|
|
659
|
+
const positive = recent.filter((e) => e.signal === 'positive').length;
|
|
660
|
+
const negative = recent.filter((e) => e.signal === 'negative').length;
|
|
661
|
+
const pct = Math.round((positive / recent.length) * 100);
|
|
662
|
+
|
|
663
|
+
const analysis = analyzeFeedback(FEEDBACK_LOG_PATH);
|
|
664
|
+
|
|
665
|
+
const lines = [
|
|
666
|
+
`## Feedback Summary (last ${recent.length})`,
|
|
667
|
+
`- Positive: ${positive}`,
|
|
668
|
+
`- Negative: ${negative}`,
|
|
669
|
+
`- Approval: ${pct}%`,
|
|
670
|
+
`- Overall approval: ${Math.round(analysis.approvalRate * 100)}%`,
|
|
671
|
+
];
|
|
672
|
+
|
|
673
|
+
if (analysis.recommendations.length > 0) {
|
|
674
|
+
lines.push('- Recommendations:');
|
|
675
|
+
analysis.recommendations.slice(0, 5).forEach((r) => lines.push(` - ${r}`));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return lines.join('\n');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function parseArgs(argv) {
|
|
682
|
+
const args = {};
|
|
683
|
+
argv.forEach((arg) => {
|
|
684
|
+
if (!arg.startsWith('--')) return;
|
|
685
|
+
const [key, ...rest] = arg.slice(2).split('=');
|
|
686
|
+
args[key] = rest.length > 0 ? rest.join('=') : true;
|
|
687
|
+
});
|
|
688
|
+
return args;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function runCli() {
|
|
692
|
+
const args = parseArgs(process.argv.slice(2));
|
|
693
|
+
|
|
694
|
+
if (args.test) {
|
|
695
|
+
runTests();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (args.capture) {
|
|
700
|
+
const result = captureFeedback({
|
|
701
|
+
signal: args.signal,
|
|
702
|
+
context: args.context || '',
|
|
703
|
+
whatWentWrong: args['what-went-wrong'],
|
|
704
|
+
whatToChange: args['what-to-change'],
|
|
705
|
+
whatWorked: args['what-worked'],
|
|
706
|
+
rubricScores: args['rubric-scores'],
|
|
707
|
+
guardrails: args.guardrails,
|
|
708
|
+
tags: args.tags,
|
|
709
|
+
skill: args.skill,
|
|
710
|
+
});
|
|
711
|
+
console.log(JSON.stringify(result, null, 2));
|
|
712
|
+
process.exit(result.accepted ? 0 : 2);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (args.analyze) {
|
|
716
|
+
console.log(JSON.stringify(analyzeFeedback(), null, 2));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (args.summary) {
|
|
721
|
+
console.log(feedbackSummary(Number(args.recent || 20)));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (args.rules) {
|
|
726
|
+
const result = writePreventionRules(args.output, Number(args.min || 2));
|
|
727
|
+
console.log(`Wrote prevention rules to ${result.path}`);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
console.log(`Usage:
|
|
732
|
+
node scripts/feedback-loop.js --capture --signal=up --context="..." --tags="verification,fix"
|
|
733
|
+
node scripts/feedback-loop.js --capture --signal=up --context="..." --rubric-scores='[{\"criterion\":\"correctness\",\"score\":4}]' --guardrails='{\"testsPassed\":true}'
|
|
734
|
+
node scripts/feedback-loop.js --analyze
|
|
735
|
+
node scripts/feedback-loop.js --summary --recent=20
|
|
736
|
+
node scripts/feedback-loop.js --rules [--min=2] [--output=path]
|
|
737
|
+
node scripts/feedback-loop.js --test`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function runTests() {
|
|
741
|
+
let passed = 0;
|
|
742
|
+
let failed = 0;
|
|
743
|
+
|
|
744
|
+
function assert(condition, name) {
|
|
745
|
+
if (condition) {
|
|
746
|
+
passed++;
|
|
747
|
+
console.log(` PASS ${name}`);
|
|
748
|
+
} else {
|
|
749
|
+
failed++;
|
|
750
|
+
console.log(` FAIL ${name}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'rlhf-loop-test-'));
|
|
755
|
+
const localFeedbackLog = path.join(tmpDir, 'feedback-log.jsonl');
|
|
756
|
+
process.env.RLHF_FEEDBACK_DIR = tmpDir;
|
|
757
|
+
|
|
758
|
+
appendJSONL(localFeedbackLog, { signal: 'positive', tags: ['testing'], skill: 'verify' });
|
|
759
|
+
appendJSONL(localFeedbackLog, { signal: 'negative', tags: ['testing'], skill: 'verify' });
|
|
760
|
+
appendJSONL(localFeedbackLog, { signal: 'positive', tags: ['testing'], skill: 'verify' });
|
|
761
|
+
|
|
762
|
+
const stats = analyzeFeedback(localFeedbackLog);
|
|
763
|
+
assert(stats.total === 3, 'analyzeFeedback counts total events');
|
|
764
|
+
assert(stats.totalPositive === 2, 'analyzeFeedback counts positives');
|
|
765
|
+
assert(stats.totalNegative === 1, 'analyzeFeedback counts negatives');
|
|
766
|
+
assert(stats.tags.testing.total === 3, 'analyzeFeedback tracks tags');
|
|
767
|
+
|
|
768
|
+
const good = captureFeedback({
|
|
769
|
+
signal: 'up',
|
|
770
|
+
context: 'Ran tests and included output',
|
|
771
|
+
whatWorked: 'Evidence-first flow',
|
|
772
|
+
tags: ['verification', 'testing'],
|
|
773
|
+
skill: 'executor',
|
|
774
|
+
});
|
|
775
|
+
assert(good.accepted, 'captureFeedback accepts valid positive feedback');
|
|
776
|
+
|
|
777
|
+
const blocked = captureFeedback({
|
|
778
|
+
signal: 'up',
|
|
779
|
+
context: 'Looks good',
|
|
780
|
+
whatWorked: 'Skipped proof',
|
|
781
|
+
tags: ['verification'],
|
|
782
|
+
rubricScores: JSON.stringify([
|
|
783
|
+
{ criterion: 'verification_evidence', score: 5, judge: 'judge-a' },
|
|
784
|
+
{ criterion: 'verification_evidence', score: 2, judge: 'judge-b', evidence: 'no test output present' },
|
|
785
|
+
]),
|
|
786
|
+
guardrails: JSON.stringify({
|
|
787
|
+
testsPassed: false,
|
|
788
|
+
pathSafety: true,
|
|
789
|
+
budgetCompliant: true,
|
|
790
|
+
}),
|
|
791
|
+
});
|
|
792
|
+
assert(!blocked.accepted, 'captureFeedback blocks unsafe positive promotion via rubric gate');
|
|
793
|
+
|
|
794
|
+
const bad = captureFeedback({ signal: 'down' });
|
|
795
|
+
assert(!bad.accepted, 'captureFeedback rejects vague negative feedback');
|
|
796
|
+
|
|
797
|
+
const summary = feedbackSummary(5);
|
|
798
|
+
assert(summary.includes('Feedback Summary'), 'feedbackSummary returns text output');
|
|
799
|
+
|
|
800
|
+
const rules = writePreventionRules(path.join(tmpDir, 'rules.md'), 1);
|
|
801
|
+
assert(rules.markdown.includes('# Prevention Rules'), 'writePreventionRules writes markdown rules');
|
|
802
|
+
const postStats = analyzeFeedback(path.join(tmpDir, 'feedback-log.jsonl'));
|
|
803
|
+
assert(postStats.rubric.blockedPromotions >= 1, 'analyzeFeedback tracks blocked rubric promotions');
|
|
804
|
+
|
|
805
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
806
|
+
delete process.env.RLHF_FEEDBACK_DIR;
|
|
807
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed\n`);
|
|
808
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
module.exports = {
|
|
812
|
+
captureFeedback,
|
|
813
|
+
analyzeFeedback,
|
|
814
|
+
buildPreventionRules,
|
|
815
|
+
writePreventionRules,
|
|
816
|
+
feedbackSummary,
|
|
817
|
+
readJSONL,
|
|
818
|
+
getFeedbackPaths,
|
|
819
|
+
inferDomain,
|
|
820
|
+
inferOutcome,
|
|
821
|
+
enrichFeedbackContext,
|
|
822
|
+
get FEEDBACK_LOG_PATH() {
|
|
823
|
+
return getFeedbackPaths().FEEDBACK_LOG_PATH;
|
|
824
|
+
},
|
|
825
|
+
get MEMORY_LOG_PATH() {
|
|
826
|
+
return getFeedbackPaths().MEMORY_LOG_PATH;
|
|
827
|
+
},
|
|
828
|
+
get SUMMARY_PATH() {
|
|
829
|
+
return getFeedbackPaths().SUMMARY_PATH;
|
|
830
|
+
},
|
|
831
|
+
get PREVENTION_RULES_PATH() {
|
|
832
|
+
return getFeedbackPaths().PREVENTION_RULES_PATH;
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
if (require.main === module) {
|
|
837
|
+
runCli();
|
|
838
|
+
}
|