46 lines
2.4 KiB
JavaScript
46 lines
2.4 KiB
JavaScript
"use strict";
|
|
|
|
function lineSlice(content, startLine, endLine) {
|
|
if (!Number.isInteger(startLine) || !Number.isInteger(endLine) || startLine < 1 || endLine < startLine) {
|
|
throw new Error("Evidence line range is invalid");
|
|
}
|
|
const lines = content.split(/\r?\n/);
|
|
if (endLine > lines.length) throw new Error("Evidence line range exceeds snapshot");
|
|
return lines.slice(startLine - 1, endLine).join("\n");
|
|
}
|
|
|
|
function verifyEvidenceItem(item, bundleByPath) {
|
|
if (!item || typeof item !== "object") throw new Error("Evidence must be an object");
|
|
const source = bundleByPath.get(item.path);
|
|
if (typeof source !== "string") throw new Error(`Evidence path is outside snapshot: ${item.path}`);
|
|
const actual = lineSlice(source, item.start_line, item.end_line);
|
|
if (actual !== item.excerpt) throw new Error(`Evidence excerpt mismatch: ${item.path}:${item.start_line}`);
|
|
return true;
|
|
}
|
|
|
|
function verifyJudgment(kind, judgment, bundleByPath, dimensions) {
|
|
if (!judgment || typeof judgment !== "object") throw new Error(`${kind} judgment missing`);
|
|
const actual = Object.keys(judgment.dimensions || {}).sort();
|
|
const expected = Object.keys(dimensions).sort();
|
|
if (actual.join("\0") !== expected.join("\0")) throw new Error(`${kind} dimension set mismatch`);
|
|
for (const name of expected) {
|
|
const dimension = judgment.dimensions[name];
|
|
if (!Number.isInteger(dimension.score) || dimension.score < 1 || dimension.score > 3) {
|
|
throw new Error(`${kind}.${name} score must be an integer in [1, 3]`);
|
|
}
|
|
if (!Array.isArray(dimension.evidence) || dimension.evidence.length === 0) {
|
|
throw new Error(`${kind}.${name} requires evidence`);
|
|
}
|
|
for (const item of dimension.evidence) verifyEvidenceItem(item, bundleByPath);
|
|
const deterministic = typeof dimension.reason_code === "string" && dimension.signals && typeof dimension.signals === "object";
|
|
const semantic = ["reasoning", "positive", "suggestion"].every((field) => typeof dimension[field] === "string" && dimension[field].trim());
|
|
if (!deterministic && !semantic) throw new Error(`${kind}.${name} requires deterministic signals or semantic reasoning`);
|
|
if (deterministic && (!Number.isFinite(dimension.confidence) || dimension.confidence < 0 || dimension.confidence > 1)) {
|
|
throw new Error(`${kind}.${name}.confidence must be in [0, 1]`);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
module.exports = { lineSlice, verifyEvidenceItem, verifyJudgment };
|