const fs = require("fs"); const crypto = require("crypto"); const { resolveData } = require("./paths"); const { roleAllows } = require("./permissions"); const { similarity, isSensitiveRequest } = require("./gate_provider"); const { atomicJson, paginate } = require("./feedback"); const PROMOTION_TARGETS = Object.freeze([ "correction", "route_alias", "predefined_answer", "eval_case", "training_export" ]); class CorrectionStore { constructor({ getConfig, verifyLink = () => false, file } = {}) { this.getConfig = getConfig || (() => ({})); this.verifyLink = verifyLink; this.file = file || resolveData("corrections", "corrections.json"); } createFromFeedback(feedback, values, actor) { if (!feedback || feedback.status !== "approved") { throw new Error("Only approved feedback can be implemented."); } const target = PROMOTION_TARGETS.includes(values.target) ? values.target : "correction"; const answer = clean(values.corrected_answer || feedback.optional_correction, 16000); if (!answer && target !== "eval_case") throw new Error("A corrected answer is required."); const expectedLink = clean(values.expected_link, 2000); this.validateLinks(answer, expectedLink); if (target === "route_alias" && !expectedLink) throw new Error("Route aliases require a verified Lumi route."); if (target === "predefined_answer" && isSensitiveRequest(feedback.user_message) && values.explicitly_safe !== true) { throw new Error("Sensitive or user-specific predefined answers must be explicitly marked safe."); } const entry = { id: crypto.randomUUID(), source_feedback_id: feedback.id, feedback_kind: feedback.feedback_kind || "strict_correction", feedback_tag: feedback.feedback_tag || "", prompt: feedback.user_message, corrected_answer: answer, rejected_answer: feedback.assistant_answer, target, route_alias: clean(values.route_alias, 500), expected_link: expectedLink, min_role: normalizeRole(values.min_role || feedback.role), permission_scope: { origin: clean(values.permission_origin || feedback.origin, 80) || "any", platform: clean(values.permission_platform || feedback.platform, 80) || "any" }, explicitly_safe: values.explicitly_safe === true, enabled: values.enabled !== false, approved: true, approved_by: String(actor.id), approved_at: new Date().toISOString(), verified_by: null, verified_at: null, active: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; const store = this.read(); store.entries.unshift(entry); this.write(store); return entry; } list({ page = 1, pageSize = 20 } = {}) { return paginate(this.read().entries, page, pageSize); } all() { return this.read().entries; } get(id) { return this.read().entries.find((entry) => entry.id === id) || null; } update(id, values) { return this.mutate(id, (entry) => { const correctedAnswer = clean(values.corrected_answer ?? entry.corrected_answer, 16000); const expectedLink = clean(values.expected_link ?? entry.expected_link, 2000); this.validateLinks(correctedAnswer, expectedLink); if (entry.target === "route_alias" && !expectedLink) throw new Error("Route aliases require a verified Lumi route."); return { ...entry, corrected_answer: correctedAnswer, route_alias: clean(values.route_alias ?? entry.route_alias, 500), expected_link: expectedLink, min_role: normalizeRole(values.min_role || entry.min_role), permission_scope: { origin: clean(values.permission_origin ?? entry.permission_scope?.origin, 80) || "any", platform: clean(values.permission_platform ?? entry.permission_scope?.platform, 80) || "any" }, explicitly_safe: values.explicitly_safe ?? entry.explicitly_safe, enabled: values.enabled ?? entry.enabled, active: false, updated_at: new Date().toISOString() }; }); } validateLinks(answer, expectedLink) { const links = [expectedLink, ...internalRouteReferences(answer)].filter(Boolean); if (links.some((link) => !this.verifyLink(link))) { throw new Error("Internal correction links must match a verified Lumi route."); } } verify(id, actor) { return this.mutate(id, (entry) => ({ ...entry, approved: true, verified_by: String(actor.id), verified_at: new Date().toISOString(), active: false, updated_at: new Date().toISOString() })); } setEnabled(id, enabled) { return this.mutate(id, (entry) => ({ ...entry, enabled: Boolean(enabled), active: false, updated_at: new Date().toISOString() })); } saveCorrections(actor) { const store = this.read(); let active = 0; for (const entry of store.entries) { entry.active = Boolean(entry.enabled && entry.approved); entry.saved_by = String(actor.id); entry.saved_at = new Date().toISOString(); if (entry.active) active += 1; } this.write(store); return { total: store.entries.length, active }; } match(input, limit = 4) { if (this.getConfig()?.improvement?.corrections_enabled === false) return []; const role = normalizeRole(input.role); return this.read().entries .filter((entry) => entry.active && entry.enabled && entry.approved) .filter((entry) => roleAllows(role, entry.min_role)) .filter((entry) => scopeAllows(entry.permission_scope, input)) .map((entry) => ({ ...entry, score: similarity(entry.prompt, input.message) })) .filter((entry) => entry.score >= 0.45) .sort((left, right) => right.score - left.score) .slice(0, limit); } findPredefined(input) { return this.match(input, 10).find((entry) => ["predefined_answer", "route_alias"].includes(entry.target) && entry.explicitly_safe === true && entry.score >= 0.82 ) || null; } context(input, limit = 4) { return this.match(input, limit) .filter((entry) => ["correction", "route_alias", "predefined_answer"].includes(entry.target)) .map((entry) => [ `Reviewed correction for a similar request (minimum role: ${entry.min_role}):`, entry.feedback_kind === "instruction_based" || entry.feedback_tag === "wrong_tool_usage" ? `Revision guidance: ${entry.feedback_tag === "wrong_tool_usage" ? "tool-calling behavior" : "instruction"}` : "", `Request: ${entry.prompt}`, `Approved answer: ${entry.corrected_answer}`, entry.expected_link ? `Verified link: ${entry.expected_link}` : "" ].filter(Boolean).join("\n")); } delete(id) { const store = this.read(); const before = store.entries.length; store.entries = store.entries.filter((entry) => entry.id !== id); if (before === store.entries.length) return false; this.write(store); return true; } mutate(id, updater) { const store = this.read(); const index = store.entries.findIndex((entry) => entry.id === id); if (index < 0) throw new Error("Correction was not found."); store.entries[index] = updater(store.entries[index]); this.write(store); return store.entries[index]; } read() { try { const parsed = JSON.parse(fs.readFileSync(this.file, "utf8")); return { entries: Array.isArray(parsed.entries) ? parsed.entries : [] }; } catch { return { entries: [] }; } } write(store) { atomicJson(this.file, { entries: store.entries.slice(0, 5000) }); } } function scopeAllows(scope = {}, input = {}) { const originAllowed = !scope.origin || scope.origin === "any" || scope.origin === input.origin; const platformAllowed = !scope.platform || scope.platform === "any" || scope.platform === input.platform; return originAllowed && platformAllowed; } function normalizeRole(value) { return ["admin", "mod", "user"].includes(value) ? value : "user"; } function clean(value, max) { return String(value || "").trim().slice(0, max); } function internalRouteReferences(value) { return [...String(value || "").matchAll(/(?:^|[\s("'`])((?:GET\s+)?\/[a-z0-9_./:-]+)/gi)] .map((match) => match[1].replace(/[.,;:!?]+$/, "")); } module.exports = { PROMOTION_TARGETS, CorrectionStore, scopeAllows, internalRouteReferences };