Files
playbook/antigravity-awesome-skills/tools/local-skill-reviewer/parity-metrics.js
T
2026-07-18 00:02:59 +00:00

395 lines
22 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const { aggregateScore, weightedJudgeScore } = require("./score");
const { DIMENSIONS } = require("./constants");
const KINDS = Object.freeze(["description", "content"]);
const DIMENSION_PATHS = Object.freeze(KINDS.flatMap((kind) => Object.keys(DIMENSIONS[kind]).map((name) => `${kind}.${name}`)));
const SCORE_BANDS = Object.freeze(["below_50", "50_to_74", "at_least_75"]);
const LEAKAGE_REASONING = /\b(?:tessl|gold(?:en)?|ground[ -]?truth|oracle|answer[ -]?key|reviewRunId|(?:expected|target|reference)\s+(?:total|score|label|level|rating)\s*(?:is|=|:)?\s*[0-9])\b/i;
function fail(message) { throw new Error(message); }
function exactKeys(value, expected, label) {
if (!value || typeof value !== "object" || Array.isArray(value)) fail(`${label} must be an object`);
const actual = Object.keys(value).sort();
const wanted = [...expected].sort();
if (actual.join("\0") !== wanted.join("\0")) fail(`${label} keys must be exactly: ${expected.join(", ")}`);
}
function finite(value, min, max, label, integer = false) {
if (!Number.isFinite(value) || value < min || value > max || (integer && !Number.isInteger(value))) fail(`${label} must be ${integer ? "an integer " : ""}in [${min}, ${max}]`);
return value;
}
function skillId(value, label) {
if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/.test(value)) fail(`${label} must contain safe lowercase hyphenated POSIX segments`);
return value;
}
function adaptGoldInput(input) {
if (Array.isArray(input)) {
if (!input.length || !Object.hasOwn(input[0], "skillId")) return input;
return input.map((row, index) => {
exactKeys(row, ["skillId", "reviewRunId", "score", "validation", "description", "content"], `gold[${index}]`);
if (typeof row.reviewRunId !== "string" || !row.reviewRunId) fail(`gold[${index}].reviewRunId must be non-empty`);
return { id: row.skillId, score: row.score, validation: row.validation, dimensions: { description: row.description, content: row.content } };
});
}
exactKeys(input, ["schemaVersion", "kind", "split", "oracle", "items"], "gold envelope");
if (input.schemaVersion !== 1 || input.kind !== "aas-tessl-parity-gold") fail("Unsupported gold envelope schema or kind");
if (!["validation", "final_blind"].includes(input.split)) fail("gold envelope split must be validation or final_blind");
exactKeys(input.oracle, ["plugin", "agent", "model"], "gold envelope oracle");
if (input.oracle.plugin !== "tessl/default-skill-review@0.1.0" || input.oracle.agent !== "claude" || input.oracle.model !== "glm-5.2") fail("gold envelope oracle cohort does not match the frozen parity oracle");
if (!Array.isArray(input.items)) fail("gold envelope items must be an array");
return input.items.map((row, index) => {
exactKeys(row, ["skillId", "reviewRunId", "score", "validation", "description", "content"], `gold.items[${index}]`);
if (typeof row.reviewRunId !== "string" || !row.reviewRunId) fail(`gold.items[${index}].reviewRunId must be non-empty`);
return { id: row.skillId, score: row.score, validation: row.validation, dimensions: { description: row.description, content: row.content } };
});
}
function normalizeGold(input) {
input = adaptGoldInput(input);
if (!Array.isArray(input) || input.length === 0) fail("gold must be a non-empty array");
const seen = new Set();
return input.map((row, index) => {
const label = `gold[${index}]`;
exactKeys(row, ["id", "score", "validation", "dimensions"], label);
const id = skillId(row.id, `${label}.id`);
if (seen.has(id)) fail(`Duplicate gold ID: ${id}`);
seen.add(id);
finite(row.score, 0, 100, `${label}.score`, true);
exactKeys(row.validation, ["normalized", "warnings", "errors", "totalChecks"], `${label}.validation`);
const warnings = finite(row.validation.warnings, 0, Number.MAX_SAFE_INTEGER, `${label}.validation.warnings`, true);
const errors = finite(row.validation.errors, 0, Number.MAX_SAFE_INTEGER, `${label}.validation.errors`, true);
const totalChecks = finite(row.validation.totalChecks, 1, Number.MAX_SAFE_INTEGER, `${label}.validation.totalChecks`, true);
const normalized = finite(row.validation.normalized, 0, 1, `${label}.validation.normalized`);
if (errors + 0.5 * warnings > totalChecks) fail(`${label}.validation error and warning penalties exceed totalChecks`);
const expectedValidation = (totalChecks - errors - 0.5 * warnings) / totalChecks;
if (Math.abs(normalized - expectedValidation) > 1e-12) fail(`${label}.validation.normalized is inconsistent with check penalties`);
exactKeys(row.dimensions, KINDS, `${label}.dimensions`);
const dimensions = {};
for (const kind of KINDS) {
const names = Object.keys(DIMENSIONS[kind]);
exactKeys(row.dimensions[kind], names, `${label}.dimensions.${kind}`);
dimensions[kind] = Object.fromEntries(names.map((name) => [name, finite(row.dimensions[kind][name], 1, 3, `${label}.dimensions.${kind}.${name}`, true)]));
}
const computed = computeTesslScore(normalized, dimensions);
if (computed !== row.score) fail(`${label}.score ${row.score} does not match public Tessl formula result ${computed}`);
return { id, score: row.score, validation: { normalized, warnings, errors, totalChecks }, dimensions };
});
}
function adaptJudgmentInput(input, judgeIndex) {
let envelope = false;
if (!Array.isArray(input)) {
exactKeys(input, ["schemaVersion", "kind", "guideVersion", "split", "items"], `judgments[${judgeIndex}] envelope`);
if (input.schemaVersion !== 1 || input.kind !== "aas-codex-tessl-level-judgments") fail(`Unsupported judgments[${judgeIndex}] envelope schema or kind`);
if (typeof input.guideVersion !== "string" || !input.guideVersion || typeof input.split !== "string" || !input.split) fail(`judgments[${judgeIndex}] envelope guideVersion and split must be non-empty`);
if (!Array.isArray(input.items)) fail(`judgments[${judgeIndex}] envelope items must be an array`);
input = input.items;
envelope = true;
}
if (!Array.isArray(input)) fail(`judgments[${judgeIndex}] must be an array or closed envelope`);
if (!input.length || !Object.hasOwn(input[0], "skillId")) return input;
return input.map((row, index) => {
const keys = envelope ? ["skillId", "bundleHash", "description", "content"] : ["skillId", "description", "content"];
exactKeys(row, keys, `judgments[${judgeIndex}]${envelope ? ".items" : ""}[${index}]`);
if (envelope && (typeof row.bundleHash !== "string" || !/^[a-f0-9]{64}$/.test(row.bundleHash))) fail(`judgments[${judgeIndex}].items[${index}].bundleHash must be a lowercase SHA-256`);
return { id: row.skillId, dimensions: { description: row.description, content: row.content } };
});
}
function normalizeJudgmentSet(input, goldIds, judgeIndex = 0) {
input = adaptJudgmentInput(input, judgeIndex);
if (!Array.isArray(input) || input.length === 0) fail(`judgments[${judgeIndex}] must be a non-empty array`);
const seen = new Set();
const rows = input.map((row, index) => {
const label = `judgments[${judgeIndex}][${index}]`;
exactKeys(row, ["id", "dimensions"], label);
const id = skillId(row.id, `${label}.id`);
if (seen.has(id)) fail(`Duplicate judgment ID in judge ${judgeIndex}: ${id}`);
seen.add(id);
exactKeys(row.dimensions, KINDS, `${label}.dimensions`);
const dimensions = {};
for (const kind of KINDS) {
const names = Object.keys(DIMENSIONS[kind]);
exactKeys(row.dimensions[kind], names, `${label}.dimensions.${kind}`);
dimensions[kind] = {};
for (const name of names) {
const cell = row.dimensions[kind][name];
exactKeys(cell, ["score", "reasoning"], `${label}.dimensions.${kind}.${name}`);
const score = finite(cell.score, 1, 3, `${label}.dimensions.${kind}.${name}.score`, true);
if (typeof cell.reasoning !== "string" || cell.reasoning.trim().length === 0) fail(`${label}.dimensions.${kind}.${name}.reasoning must be non-empty`);
if (LEAKAGE_REASONING.test(cell.reasoning)) fail(`${label}.dimensions.${kind}.${name}.reasoning contains apparent gold leakage`);
dimensions[kind][name] = { score, reasoning: cell.reasoning };
}
}
return { id, dimensions };
});
const expected = [...goldIds].sort();
const actual = [...seen].sort();
const missing = expected.filter((id) => !seen.has(id));
const extra = actual.filter((id) => !goldIds.has(id));
if (missing.length || extra.length) fail(`Judge ${judgeIndex} ID mismatch; missing=[${missing.join(",")}], extra=[${extra.join(",")}]`);
return rows;
}
function computeTesslScore(validationNormalized, dimensions) {
const wrapped = (kind) => Object.fromEntries(Object.entries(dimensions[kind]).map(([name, value]) => [name, { score: typeof value === "number" ? value : value.score }]));
return aggregateScore(validationNormalized, weightedJudgeScore("description", wrapped("description")), weightedJudgeScore("content", wrapped("content")));
}
function flattenDimensions(dimensions) {
return Object.fromEntries(DIMENSION_PATHS.map((path) => {
const [kind, name] = path.split(".");
const value = dimensions[kind][name];
return [path, typeof value === "number" ? value : value.score];
}));
}
function ordinalMedian(values) {
const sorted = [...values].sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[middle] : Math.round((sorted[middle - 1] + sorted[middle]) / 2);
}
function majorityThenOrdinalMedian(values) {
const counts = new Map([1, 2, 3].map((level) => [level, values.filter((value) => value === level).length]));
const majority = [...counts].find(([, count]) => count > values.length / 2);
return majority ? majority[0] : ordinalMedian(values);
}
function aggregateJudgments(judgmentSets) {
const maps = judgmentSets.map((rows) => new Map(rows.map((row) => [row.id, row])));
return judgmentSets[0].map((row) => {
const dimensions = {};
for (const kind of KINDS) {
dimensions[kind] = {};
for (const name of Object.keys(DIMENSIONS[kind])) {
const values = maps.map((map) => map.get(row.id).dimensions[kind][name].score);
dimensions[kind][name] = { score: majorityThenOrdinalMedian(values), reasoning: `Strict majority, falling back to ordinal median, across ${values.length} closed judgments.` };
}
}
return { id: row.id, dimensions };
});
}
function confusion(goldValues, predictedValues) {
const matrix = Array.from({ length: 3 }, () => [0, 0, 0]);
for (let index = 0; index < goldValues.length; index += 1) matrix[goldValues[index] - 1][predictedValues[index] - 1] += 1;
return matrix;
}
function f1FromConfusion(matrix) {
return [0, 1, 2].map((level) => {
const tp = matrix[level][level];
const fp = matrix.reduce((sum, row, rowIndex) => sum + (rowIndex === level ? 0 : row[level]), 0);
const fn = matrix[level].reduce((sum, value, columnIndex) => sum + (columnIndex === level ? 0 : value), 0);
const denominator = 2 * tp + fp + fn;
return denominator ? (2 * tp) / denominator : 0;
});
}
function quadraticWeightedKappa(matrix) {
const count = matrix.flat().reduce((sum, value) => sum + value, 0);
if (!count) return null;
const rows = matrix.map((row) => row.reduce((sum, value) => sum + value, 0));
const columns = [0, 1, 2].map((column) => matrix.reduce((sum, row) => sum + row[column], 0));
let observed = 0;
let expected = 0;
for (let row = 0; row < 3; row += 1) {
for (let column = 0; column < 3; column += 1) {
const weight = ((row - column) ** 2) / 4;
observed += weight * matrix[row][column] / count;
expected += weight * rows[row] * columns[column] / (count * count);
}
}
if (expected === 0) return observed === 0 ? 1 : 0;
return 1 - observed / expected;
}
function band(score) { return score < 50 ? SCORE_BANDS[0] : score < 75 ? SCORE_BANDS[1] : SCORE_BANDS[2]; }
function mean(values) { return values.reduce((sum, value) => sum + value, 0) / values.length; }
function metricsForPredictions(gold, predictions) {
const predictionMap = new Map(predictions.map((row) => [row.id, row]));
const dimensionPairs = Object.fromEntries(DIMENSION_PATHS.map((path) => [path, { gold: [], predicted: [] }]));
const scoreRows = [];
for (const item of gold) {
const prediction = predictionMap.get(item.id);
const goldFlat = flattenDimensions(item.dimensions);
const predictedFlat = flattenDimensions(prediction.dimensions);
for (const path of DIMENSION_PATHS) {
dimensionPairs[path].gold.push(goldFlat[path]);
dimensionPairs[path].predicted.push(predictedFlat[path]);
}
const predictedScore = computeTesslScore(item.validation.normalized, prediction.dimensions);
scoreRows.push({ id: item.id, gold: item.score, predicted: predictedScore, error: predictedScore - item.score, goldBand: band(item.score), predictedBand: band(predictedScore) });
}
const perDimension = {};
const combinedGold = [];
const combinedPredicted = [];
for (const path of DIMENSION_PATHS) {
const pair = dimensionPairs[path];
const matrix = confusion(pair.gold, pair.predicted);
combinedGold.push(...pair.gold);
combinedPredicted.push(...pair.predicted);
perDimension[path] = {
exactAgreement: mean(pair.gold.map((value, index) => Number(value === pair.predicted[index]))),
macroF1: mean(f1FromConfusion(matrix)),
quadraticWeightedKappa: quadraticWeightedKappa(matrix),
confusionMatrix: matrix,
};
}
const combinedMatrix = confusion(combinedGold, combinedPredicted);
const absoluteErrors = scoreRows.map((row) => Math.abs(row.error));
const squaredErrors = scoreRows.map((row) => row.error ** 2);
return {
skills: gold.length,
labels: combinedGold.length,
exactMacroAgreement: mean(Object.values(perDimension).map((item) => item.exactAgreement)),
perDimension,
macroF1: mean(f1FromConfusion(combinedMatrix)),
quadraticWeightedKappa: quadraticWeightedKappa(combinedMatrix),
confusionMatrix: combinedMatrix,
score: { mae: mean(absoluteErrors), rmse: Math.sqrt(mean(squaredErrors)), bandAgreement: mean(scoreRows.map((row) => Number(row.goldBand === row.predictedBand))), rows: scoreRows },
};
}
function majorityLevelBaseline(gold) {
const levels = {};
for (const path of DIMENSION_PATHS) {
const counts = [1, 2, 3].map((level) => gold.filter((row) => flattenDimensions(row.dimensions)[path] === level).length);
const maximum = Math.max(...counts);
levels[path] = counts.indexOf(maximum) + 1;
}
const predictions = gold.map((row) => ({ id: row.id, dimensions: Object.fromEntries(KINDS.map((kind) => [kind, Object.fromEntries(Object.keys(DIMENSIONS[kind]).map((name) => [name, { score: levels[`${kind}.${name}`] }]))])) }));
const metrics = metricsForPredictions(gold, predictions);
return { levels, exactMacroAgreement: metrics.exactMacroAgreement, macroF1: metrics.macroF1, quadraticWeightedKappa: metrics.quadraticWeightedKappa, scoreMae: metrics.score.mae, scoreRmse: metrics.score.rmse, scoreBandAgreement: metrics.score.bandAgreement };
}
function mulberry32(seed) {
let state = seed >>> 0;
return () => { state += 0x6D2B79F5; let value = state; value = Math.imul(value ^ value >>> 15, value | 1); value ^= value + Math.imul(value ^ value >>> 7, value | 61); return ((value ^ value >>> 14) >>> 0) / 4294967296; };
}
function hashSeed(value) {
let hash = 2166136261;
for (const character of String(value)) { hash ^= character.charCodeAt(0); hash = Math.imul(hash, 16777619); }
return hash >>> 0;
}
function percentile(values, probability) {
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
if (!sorted.length) return null;
return sorted[Math.floor((sorted.length - 1) * probability)];
}
function bootstrapConfidenceIntervals(gold, predictions, { iterations = 1000, seed = "aas-parity-bootstrap-v1" } = {}) {
finite(iterations, 1, 100000, "bootstrap iterations", true);
const random = mulberry32(hashSeed(seed));
const predictionMap = new Map(predictions.map((row) => [row.id, row]));
const samples = { exactMacroAgreement: [], macroF1: [], quadraticWeightedKappa: [], scoreMae: [], scoreRmse: [], scoreBandAgreement: [] };
for (let iteration = 0; iteration < iterations; iteration += 1) {
const sampledGold = [];
const sampledPredictions = [];
for (let index = 0; index < gold.length; index += 1) {
const selected = gold[Math.floor(random() * gold.length)];
const bootstrapId = `${selected.id}-${index}`;
sampledGold.push({ ...selected, id: bootstrapId });
sampledPredictions.push({ ...predictionMap.get(selected.id), id: bootstrapId });
}
const metrics = metricsForPredictions(sampledGold, sampledPredictions);
samples.exactMacroAgreement.push(metrics.exactMacroAgreement);
samples.macroF1.push(metrics.macroF1);
samples.quadraticWeightedKappa.push(metrics.quadraticWeightedKappa);
samples.scoreMae.push(metrics.score.mae);
samples.scoreRmse.push(metrics.score.rmse);
samples.scoreBandAgreement.push(metrics.score.bandAgreement);
}
return { method: "clustered-by-skill-percentile", iterations, seed: String(seed), confidence: 0.95, intervals: Object.fromEntries(Object.entries(samples).map(([name, values]) => [name, { lower: percentile(values, 0.025), upper: percentile(values, 0.975) }])) };
}
function normalizeStrata(manifest, goldIds) {
if (manifest === undefined || manifest === null) return null;
let rows;
const direct = Array.isArray(manifest);
if (direct) rows = manifest;
else if (manifest && manifest.splits && typeof manifest.splits === "object") rows = Object.values(manifest.splits).flat();
else fail("strata manifest must be an array or contain split arrays");
const selected = rows.filter((row) => goldIds.has(row.id));
const seen = new Set(selected.map((row) => row.id));
if (seen.size !== selected.length) fail("Strata manifest contains duplicate gold IDs");
const missing = [...goldIds].filter((id) => !seen.has(id));
if (missing.length) fail(`Strata manifest missing gold IDs: ${missing.join(",")}`);
if (direct) {
const extra = rows.filter((row) => !goldIds.has(row.id)).map((row) => row.id);
if (extra.length) fail(`Strata manifest has extra IDs: ${extra.join(",")}`);
}
for (const row of selected) {
if (typeof row.primaryStratum !== "string" || !row.primaryStratum) fail(`Strata row ${row.id} lacks primaryStratum`);
if (row.overlays !== undefined && (!row.overlays || typeof row.overlays !== "object" || Array.isArray(row.overlays) || Object.values(row.overlays).some((value) => typeof value !== "boolean"))) fail(`Strata row ${row.id} has invalid overlays`);
}
return selected;
}
function stratumMetrics(gold, predictions, strata) {
if (!strata) return null;
const groups = {};
for (const row of strata) {
(groups[`primary:${row.primaryStratum}`] ||= []).push(row.id);
for (const [name, enabled] of Object.entries(row.overlays || {})) if (enabled) (groups[`overlay:${name}`] ||= []).push(row.id);
}
const predictionMap = new Map(predictions.map((row) => [row.id, row]));
const goldMap = new Map(gold.map((row) => [row.id, row]));
return Object.fromEntries(Object.entries(groups).sort().map(([name, ids]) => [name, metricsForPredictions(ids.map((id) => goldMap.get(id)), ids.map((id) => predictionMap.get(id)))]));
}
function evaluateParity({ gold, judgments, strata, bootstrapIterations = 1000, seed = "aas-parity-bootstrap-v1" }) {
const normalizedGold = normalizeGold(gold);
if (!Array.isArray(judgments) || judgments.length === 0) fail("judgments must contain one or more judge arrays");
const goldIds = new Set(normalizedGold.map((row) => row.id));
const normalizedJudgments = judgments.map((rows, index) => normalizeJudgmentSet(rows, goldIds, index));
const normalizedStrata = normalizeStrata(strata, goldIds);
const singleJudges = normalizedJudgments.map((rows, index) => {
const metrics = metricsForPredictions(normalizedGold, rows);
return { judge: index, metrics, bootstrap: bootstrapConfidenceIntervals(normalizedGold, rows, { iterations: bootstrapIterations, seed: `${seed}:judge:${index}` }), strata: stratumMetrics(normalizedGold, rows, normalizedStrata) };
});
const ensembleRows = aggregateJudgments(normalizedJudgments);
const ensembleMetrics = metricsForPredictions(normalizedGold, ensembleRows);
return {
schemaVersion: 1,
dimensionOrder: DIMENSION_PATHS,
scoreBands: SCORE_BANDS,
majorityLevelBaseline: majorityLevelBaseline(normalizedGold),
primary: { method: "single-primary-judge", judge: 0, ...singleJudges[0] },
singleJudges,
ensemble: { role: "diagnostic-only", method: "strict-majority-then-ordinal-median", judges: normalizedJudgments.length, metrics: ensembleMetrics, bootstrap: bootstrapConfidenceIntervals(normalizedGold, ensembleRows, { iterations: bootstrapIterations, seed: `${seed}:ensemble` }), strata: stratumMetrics(normalizedGold, ensembleRows, normalizedStrata) },
};
}
function main(argv) {
const args = [...argv];
const goldPath = args.shift();
if (!goldPath) fail("Usage: parity-metrics.js GOLD.json JUDGE.json [JUDGE.json ...] [--strata FILE] [--bootstrap N] [--seed VALUE]");
const judgePaths = [];
let strataPath;
let bootstrapIterations = 1000;
let seed = "aas-parity-bootstrap-v1";
while (args.length) {
const token = args.shift();
if (token === "--strata") strataPath = args.shift() || fail("--strata requires a file");
else if (token === "--bootstrap") bootstrapIterations = Number(args.shift());
else if (token === "--seed") seed = args.shift() || fail("--seed requires a value");
else if (token.startsWith("--")) fail(`Unknown option: ${token}`);
else judgePaths.push(token);
}
if (!judgePaths.length) fail("At least one judgment file is required");
const read = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
process.stdout.write(`${JSON.stringify(evaluateParity({ gold: read(goldPath), judgments: judgePaths.map(read), strata: strataPath ? read(strataPath) : undefined, bootstrapIterations, seed }), null, 2)}\n`);
}
if (require.main === module) {
try { main(process.argv.slice(2)); } catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }
}
module.exports = { DIMENSION_PATHS, adaptGoldInput, adaptJudgmentInput, aggregateJudgments, bootstrapConfidenceIntervals, computeTesslScore, evaluateParity, majorityLevelBaseline, majorityThenOrdinalMedian, metricsForPredictions, normalizeGold, normalizeJudgmentSet, ordinalMedian, quadraticWeightedKappa };