133 lines
5.9 KiB
JavaScript
133 lines
5.9 KiB
JavaScript
// Test-only HTTP provider. Controlled wording/evidence exercises the API contract;
|
|
// this fixture does not measure a model's semantic accuracy and never calls a provider.
|
|
import http from "node:http";
|
|
|
|
const args = process.argv.slice(2);
|
|
if (args.length && (args.length !== 2 || args[0] !== "--port")) {
|
|
throw new Error("Usage: node cv-ai-provider-fixture.mjs [--port 18133]");
|
|
}
|
|
const port = args.length ? Number(args[1]) : 18133;
|
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid fixture port.");
|
|
|
|
const fabricated = "Managed 50 engineers at Microsoft and increased revenue 300%.";
|
|
let mode = "supported";
|
|
const counts = { rewrite: 0, evaluate: 0 };
|
|
const calls = [];
|
|
|
|
function supportedRewrite(value) {
|
|
return value.replace(/\bBuilt\b/g, "Developed").replace(/\bbuilt\b/g, "developed")
|
|
.replace(/\breliable\b/g, "dependable").replace(/\bUtviklet\b/g, "Bygget")
|
|
.replace(/\butviklet\b/g, "bygget");
|
|
}
|
|
|
|
function equivalent(value) {
|
|
return supportedRewrite(value).trim();
|
|
}
|
|
|
|
function parts(value) {
|
|
if (typeof value !== "string" || !value.trim()) throw new Error("Nonempty source/proposal required.");
|
|
if (!value.trimStart().startsWith("[")) return value.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
const blocks = JSON.parse(value);
|
|
if (!Array.isArray(blocks)) throw new Error("CV blocks must be an array.");
|
|
const values = [];
|
|
for (const block of blocks) {
|
|
if (!block || typeof block !== "object" || Array.isArray(block)) throw new Error("Invalid CV block.");
|
|
for (const [key, field] of Object.entries(block)) {
|
|
if (key === "id") {
|
|
if (typeof field !== "string") throw new Error("Invalid block id.");
|
|
} else if (key === "title" || key === "subtitle") {
|
|
add(field);
|
|
} else if (key === "items" && Array.isArray(field)) {
|
|
field.forEach(add);
|
|
} else throw new Error("Invalid block field.");
|
|
}
|
|
}
|
|
return values;
|
|
function add(value) {
|
|
if (value === null) return;
|
|
if (typeof value !== "string") throw new Error("Invalid block value.");
|
|
if (value.trim()) values.push(value);
|
|
}
|
|
}
|
|
|
|
function rewrite(body) {
|
|
if (typeof body.text !== "string" || !body.text.trim()) throw new Error("Nonempty text required.");
|
|
const outputMode = body.output_mode ?? "text";
|
|
if (outputMode !== "text" && outputMode !== "cv_blocks_json") throw new Error("Invalid output_mode.");
|
|
if (outputMode === "text") return { rewritten_text: mode === "fabricated" ? fabricated : supportedRewrite(body.text) };
|
|
parts(body.text);
|
|
const blocks = JSON.parse(body.text).map((block) => Object.fromEntries(Object.entries(block).map(([key, value]) => [
|
|
key, key === "id" ? value : typeof value === "string" ? supportedRewrite(value)
|
|
: Array.isArray(value) ? value.map(supportedRewrite) : value,
|
|
])));
|
|
if (!blocks.length) throw new Error("Nonempty CV blocks required.");
|
|
if (mode === "fabricated") {
|
|
const target = blocks.find((block) => block.items?.length) ?? blocks[0];
|
|
target.items = [fabricated];
|
|
}
|
|
return { rewritten_text: JSON.stringify(blocks) };
|
|
}
|
|
|
|
function evaluate(body) {
|
|
const source = parts(body.candidate_source);
|
|
const proposed = parts(body.proposal);
|
|
const claims = proposed.map((text) => {
|
|
const quote = source.find((value) => equivalent(value) === equivalent(text));
|
|
return { text, verdict: quote ? "supported" : "unsupported", sourceQuotes: quote ? [quote] : [] };
|
|
});
|
|
return { assessment: {
|
|
verdict: claims.length && claims.every((claim) => claim.verdict === "supported") ? "supported" : "unsupported",
|
|
coverageComplete: true,
|
|
claims,
|
|
} };
|
|
}
|
|
|
|
function send(response, status, body) {
|
|
response.writeHead(status, {
|
|
"Content-Type": "application/json",
|
|
"X-Ai-Provider": "loopback-fixture",
|
|
"X-Ai-Model": "deterministic-cv-proof",
|
|
"X-Ai-Route-Reason": "test-fixture",
|
|
});
|
|
response.end(JSON.stringify(body));
|
|
}
|
|
|
|
const server = http.createServer(async (request, response) => {
|
|
const path = request.url?.split("?")[0];
|
|
if (request.method === "GET" && path === "/health") return send(response, 200, { status: "ok", fixture: true });
|
|
if (request.method === "GET" && path === "/fixture/state") return send(response, 200, { mode, counts, calls });
|
|
if (request.method !== "POST" || !["/fixture/mode", "/fixture/reset", "/cv/rewrite", "/cv/evaluate-rewrite"].includes(path)) {
|
|
return send(response, 404, { error: "unknown_fixture_route" });
|
|
}
|
|
try {
|
|
const chunks = [];
|
|
let size = 0;
|
|
for await (const chunk of request) {
|
|
size += chunk.length;
|
|
if (size > 256_000) return send(response, 413, { error: "fixture_body_too_large" });
|
|
chunks.push(chunk);
|
|
}
|
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
const body = raw.trim() ? JSON.parse(raw) : {};
|
|
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("JSON object required.");
|
|
if (path === "/fixture/mode" || path === "/fixture/reset") {
|
|
const next = body.mode ?? (path === "/fixture/reset" ? "supported" : undefined);
|
|
if (next !== "supported" && next !== "fabricated") throw new Error("Invalid fixture mode.");
|
|
mode = next;
|
|
if (path === "/fixture/reset") { counts.rewrite = 0; counts.evaluate = 0; calls.length = 0; }
|
|
return send(response, 200, { mode, counts });
|
|
}
|
|
counts[path === "/cv/rewrite" ? "rewrite" : "evaluate"] += 1;
|
|
const result = path === "/cv/rewrite" ? rewrite(body) : evaluate(body);
|
|
calls.push({ path, mode, body, response: result });
|
|
return send(response, 200, result);
|
|
} catch (error) {
|
|
return send(response, 400, { error: error instanceof Error ? error.message : "Invalid fixture request." });
|
|
}
|
|
});
|
|
|
|
server.listen(port, "127.0.0.1", () => console.log(`CV AI provider fixture: http://127.0.0.1:${port}`));
|
|
server.on("error", (error) => { console.error(error.message); process.exitCode = 1; });
|
|
process.on("SIGTERM", () => server.close(() => process.exit(0)));
|
|
process.on("SIGINT", () => server.close(() => process.exit(0)));
|