Prepare scanner branch for merge
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
function argValue(name, fallback = "") {
|
||||
const prefix = `--${name}=`;
|
||||
const match = process.argv.find((entry) => entry.startsWith(prefix));
|
||||
return match ? match.slice(prefix.length) : fallback;
|
||||
}
|
||||
|
||||
function hasFlag(name) {
|
||||
return process.argv.includes(`--${name}`);
|
||||
}
|
||||
|
||||
function defaultAssessmentRoot() {
|
||||
return path.join(process.cwd(), "outputs", "live-soak");
|
||||
}
|
||||
|
||||
function findLatestAssessment(rootDir = defaultAssessmentRoot()) {
|
||||
const resolvedRoot = path.resolve(rootDir);
|
||||
if (!fs.existsSync(resolvedRoot)) throw new Error(`Assessment root not found: ${resolvedRoot}`);
|
||||
const candidates = fs.readdirSync(resolvedRoot, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => {
|
||||
const assessmentPath = path.join(resolvedRoot, entry.name, "scan-performance-assessment.json");
|
||||
if (!fs.existsSync(assessmentPath)) return null;
|
||||
const stat = fs.statSync(assessmentPath);
|
||||
return { assessmentPath, runName: entry.name, mtimeMs: stat.mtimeMs };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => {
|
||||
const mtimeDiff = right.mtimeMs - left.mtimeMs;
|
||||
if (Math.abs(mtimeDiff) > 1) return mtimeDiff;
|
||||
return right.runName.localeCompare(left.runName);
|
||||
});
|
||||
if (candidates.length === 0) throw new Error(`No scan-performance-assessment.json found under: ${resolvedRoot}`);
|
||||
return candidates[0].assessmentPath;
|
||||
}
|
||||
|
||||
function loadAssessment(inputPath) {
|
||||
if (!inputPath) throw new Error("Missing assessment file. Pass --input=<scan-performance-assessment.json>.");
|
||||
return JSON.parse(fs.readFileSync(path.resolve(inputPath), "utf8").replace(/^\uFEFF/, ""));
|
||||
}
|
||||
|
||||
function findLimitAssessment(assessment, limit) {
|
||||
if (limit === 100 && assessment?.goal100) return assessment.goal100;
|
||||
const limits = Array.isArray(assessment?.limits) ? assessment.limits : [];
|
||||
return limits.find((entry) => Number(entry?.limit) === limit) || null;
|
||||
}
|
||||
|
||||
function validateAssessment(assessment, options = {}) {
|
||||
const errors = [];
|
||||
const expectedWinner = options.expectedWinner || "any";
|
||||
const expectedLimit = options.limit === undefined ? 100 : Number(options.limit);
|
||||
if (!assessment || typeof assessment !== "object") {
|
||||
return { ok: false, errors: ["Assessment must be a JSON object."] };
|
||||
}
|
||||
|
||||
if (!Number.isInteger(expectedLimit) || expectedLimit < 1) {
|
||||
errors.push(`--limit must be a positive integer, got ${options.limit}.`);
|
||||
}
|
||||
|
||||
const limitAssessment = findLimitAssessment(assessment, expectedLimit);
|
||||
if (!limitAssessment || typeof limitAssessment !== "object") {
|
||||
errors.push(`Missing limit=${expectedLimit} assessment.`);
|
||||
}
|
||||
|
||||
if (expectedLimit === 100 && assessment.goal100Decision !== `qualified-comparison: winner=${limitAssessment?.winnerEngine}`) {
|
||||
errors.push(`goal100Decision is not a qualified comparison: ${assessment.goal100Decision || "<missing>"}`);
|
||||
}
|
||||
|
||||
if (limitAssessment?.limit !== expectedLimit) {
|
||||
errors.push(`limit assessment must be ${expectedLimit}, got ${limitAssessment?.limit ?? "<missing>"}.`);
|
||||
}
|
||||
if (limitAssessment?.comparisonComplete !== true) errors.push(`limit=${expectedLimit}.comparisonComplete must be true.`);
|
||||
if (limitAssessment?.winnerQualified !== true) errors.push(`limit=${expectedLimit}.winnerQualified must be true.`);
|
||||
if (expectedWinner !== "any" && limitAssessment?.winnerEngine !== expectedWinner) {
|
||||
errors.push(`Expected winner '${expectedWinner}', got '${limitAssessment?.winnerEngine ?? "<missing>"}'.`);
|
||||
}
|
||||
|
||||
const engines = Array.isArray(limitAssessment?.engines) ? limitAssessment.engines : [];
|
||||
const engineNames = new Set(engines.map((entry) => entry?.engine));
|
||||
for (const required of ["current", "ik-traineddata"]) {
|
||||
if (!engineNames.has(required)) errors.push(`limit=${expectedLimit} is missing engine result: ${required}.`);
|
||||
}
|
||||
|
||||
const winner = engines.find((entry) => entry?.engine === limitAssessment?.winnerEngine);
|
||||
if (!winner) {
|
||||
errors.push(`Winner engine is missing from limit=${expectedLimit}.engines: ${limitAssessment?.winnerEngine ?? "<missing>"}.`);
|
||||
} else {
|
||||
const winnerMissRate = Number(winner.missRate);
|
||||
const winnerReviewRate = Number(winner.reviewRate);
|
||||
const summaryWinnerMissRate = Number(limitAssessment?.winnerMissRate);
|
||||
const summaryWinnerReviewRate = Number(limitAssessment?.winnerReviewRate);
|
||||
const winnerActiveAverageMsPerParsed = Number(limitAssessment?.winnerActiveAverageMsPerParsed);
|
||||
const winnerActiveProjectedMsFor100 = Number(limitAssessment?.winnerActiveProjectedMsFor100);
|
||||
if (winner.qualified !== true) errors.push("Winner engine result must be qualified.");
|
||||
if (!Number.isFinite(winnerMissRate)) {
|
||||
errors.push(`Winner missRate must be a finite number, got ${winner.missRate ?? "<missing>"}.`);
|
||||
} else if (winnerMissRate > 0.02) {
|
||||
errors.push(`Winner missRate exceeds 2%: ${winner.missRate}.`);
|
||||
}
|
||||
if (!Number.isFinite(winnerReviewRate)) {
|
||||
errors.push(`Winner reviewRate must be a finite number, got ${winner.reviewRate ?? "<missing>"}.`);
|
||||
} else if (winnerReviewRate > 0.15) {
|
||||
errors.push(`Winner reviewRate exceeds 15%: ${winner.reviewRate}.`);
|
||||
}
|
||||
if (!Number.isFinite(summaryWinnerMissRate)) {
|
||||
errors.push(`Winner summary missRate must be a finite number, got ${limitAssessment?.winnerMissRate ?? "<missing>"}.`);
|
||||
} else if (summaryWinnerMissRate > 0.02) {
|
||||
errors.push(`Winner summary missRate exceeds 2%: ${limitAssessment.winnerMissRate}.`);
|
||||
}
|
||||
if (!Number.isFinite(summaryWinnerReviewRate)) {
|
||||
errors.push(`Winner summary reviewRate must be a finite number, got ${limitAssessment?.winnerReviewRate ?? "<missing>"}.`);
|
||||
} else if (summaryWinnerReviewRate > 0.15) {
|
||||
errors.push(`Winner summary reviewRate exceeds 15%: ${limitAssessment.winnerReviewRate}.`);
|
||||
}
|
||||
if (!Number.isFinite(winnerActiveAverageMsPerParsed) || winnerActiveAverageMsPerParsed <= 0) {
|
||||
errors.push(`Winner active average timing must be a positive finite number, got ${limitAssessment?.winnerActiveAverageMsPerParsed ?? "<missing>"}.`);
|
||||
}
|
||||
if (!Number.isFinite(winnerActiveProjectedMsFor100) || winnerActiveProjectedMsFor100 <= 0) {
|
||||
errors.push(`Winner projected100 timing must be a positive finite number, got ${limitAssessment?.winnerActiveProjectedMsFor100 ?? "<missing>"}.`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: errors.length === 0,
|
||||
errors,
|
||||
createdAt: assessment.createdAt || "",
|
||||
limit: expectedLimit,
|
||||
winnerEngine: limitAssessment?.winnerEngine,
|
||||
winnerActiveAverageMsPerParsed: limitAssessment?.winnerActiveAverageMsPerParsed,
|
||||
winnerActiveProjectedMsFor100: limitAssessment?.winnerActiveProjectedMsFor100,
|
||||
winnerMissRate: limitAssessment?.winnerMissRate,
|
||||
winnerReviewRate: limitAssessment?.winnerReviewRate,
|
||||
};
|
||||
}
|
||||
|
||||
function formatSummary(result) {
|
||||
const status = result.ok ? "PASS" : "FAIL";
|
||||
const lines = [
|
||||
`scan assessment: ${status}`,
|
||||
`input: ${result.inputPath || "unknown"}`,
|
||||
`createdAt: ${result.createdAt || "unknown"}`,
|
||||
`limit: ${result.limit ?? "unknown"}`,
|
||||
`winner: ${result.winnerEngine || "unknown"}`,
|
||||
`activeAvg: ${result.winnerActiveAverageMsPerParsed ?? "unknown"}ms/artifact`,
|
||||
`projected100: ${result.winnerActiveProjectedMsFor100 ?? "unknown"}ms`,
|
||||
`missRate: ${result.winnerMissRate ?? "unknown"}`,
|
||||
`reviewRate: ${result.winnerReviewRate ?? "unknown"}`,
|
||||
];
|
||||
if (!result.ok) {
|
||||
lines.push("errors:");
|
||||
for (const error of result.errors) lines.push(`- ${error}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function main() {
|
||||
const inputPath = hasFlag("latest")
|
||||
? findLatestAssessment(argValue("root", defaultAssessmentRoot()))
|
||||
: argValue("input", process.argv[2] || "");
|
||||
const expectedWinner = argValue("expect-winner", "any");
|
||||
if (!["any", "current", "ik-traineddata"].includes(expectedWinner)) {
|
||||
throw new Error("--expect-winner must be one of: any, current, ik-traineddata.");
|
||||
}
|
||||
const limit = Number(argValue("limit", "100"));
|
||||
const assessment = loadAssessment(inputPath);
|
||||
const result = validateAssessment(assessment, { expectedWinner, limit });
|
||||
const payload = { inputPath: path.resolve(inputPath), ...result };
|
||||
console.log(hasFlag("summary") ? formatSummary(payload) : JSON.stringify(payload, null, 2));
|
||||
if (!result.ok) process.exit(1);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findLatestAssessment,
|
||||
findLimitAssessment,
|
||||
formatSummary,
|
||||
loadAssessment,
|
||||
validateAssessment,
|
||||
};
|
||||
Reference in New Issue
Block a user