191 lines
7.8 KiB
JavaScript
191 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const verificationRoot = path.resolve(here, "..", "..");
|
|
const repositoryRoot = path.resolve(verificationRoot, "..", "..");
|
|
const benchmarkRoot = path.join(verificationRoot, "baseline", "v1", "benchmark");
|
|
const frozen = process.argv.includes("--require-approvals");
|
|
const failures = [];
|
|
|
|
function fail(code, detail) {
|
|
failures.push({ code, detail });
|
|
}
|
|
|
|
function readJson(file) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
} catch (error) {
|
|
fail("AAS_BENCHMARK_INVALID_JSON", `${path.relative(verificationRoot, file)}: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function walkJson(directory) {
|
|
if (!fs.existsSync(directory)) return [];
|
|
return fs.readdirSync(directory, { withFileTypes: true })
|
|
.flatMap((entry) => {
|
|
const target = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) return walkJson(target);
|
|
return entry.isFile() && entry.name.endsWith(".json") ? [target] : [];
|
|
})
|
|
.sort();
|
|
}
|
|
|
|
function canonicalize(value) {
|
|
if (Array.isArray(value)) return value.map(canonicalize);
|
|
if (value && typeof value === "object") {
|
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function pairDigest(caseData, goldData) {
|
|
const reviewNeutralGold = { ...goldData, reviews: [] };
|
|
const bytes = JSON.stringify(canonicalize({ case: caseData, gold: reviewNeutralGold }));
|
|
return `sha256-${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
}
|
|
|
|
const index = readJson(path.join(benchmarkRoot, "held-out-index.json"));
|
|
const catalog = readJson(path.join(repositoryRoot, "data", "skills_index.json"));
|
|
if (!index || !catalog) {
|
|
console.error(JSON.stringify({ ok: false, failures }, null, 2));
|
|
process.exit(1);
|
|
}
|
|
|
|
const descriptors = new Map(index.cases.map((entry) => [entry.caseId, entry]));
|
|
const catalogById = new Map(catalog.map((entry) => [entry.id, entry]));
|
|
const caseRoot = path.join(benchmarkRoot, "cases", "held-out");
|
|
const goldRoot = path.join(benchmarkRoot, "gold", "held-out");
|
|
const caseFiles = walkJson(caseRoot);
|
|
const goldFiles = walkJson(goldRoot);
|
|
const seenIds = new Set();
|
|
const seenFingerprints = new Set();
|
|
|
|
if (caseFiles.length !== 180) fail("AAS_BENCHMARK_CASE_COUNT", `expected 180, found ${caseFiles.length}`);
|
|
if (goldFiles.length !== 180) fail("AAS_BENCHMARK_GOLD_COUNT", `expected 180, found ${goldFiles.length}`);
|
|
|
|
for (const caseFile of caseFiles) {
|
|
const relative = path.relative(caseRoot, caseFile);
|
|
const goldFile = path.join(goldRoot, relative);
|
|
const caseData = readJson(caseFile);
|
|
const goldData = readJson(goldFile);
|
|
if (!caseData || !goldData) continue;
|
|
const descriptor = descriptors.get(caseData.caseId);
|
|
|
|
if (!descriptor) fail("AAS_BENCHMARK_UNKNOWN_CASE", caseData.caseId);
|
|
if (seenIds.has(caseData.caseId)) fail("AAS_BENCHMARK_DUPLICATE_CASE", caseData.caseId);
|
|
seenIds.add(caseData.caseId);
|
|
if (seenFingerprints.has(caseData.taskFamilyFingerprint)) {
|
|
fail("AAS_BENCHMARK_DUPLICATE_FINGERPRINT", caseData.taskFamilyFingerprint);
|
|
}
|
|
seenFingerprints.add(caseData.taskFamilyFingerprint);
|
|
|
|
if (!descriptor) continue;
|
|
if (descriptor.inputPath && descriptor.inputPath !== path.relative(benchmarkRoot, caseFile)) {
|
|
fail("AAS_BENCHMARK_PATH_MISMATCH", `${caseData.caseId}/input`);
|
|
}
|
|
if (descriptor.goldPath && descriptor.goldPath !== path.relative(benchmarkRoot, goldFile)) {
|
|
fail("AAS_BENCHMARK_PATH_MISMATCH", `${caseData.caseId}/gold`);
|
|
}
|
|
if (caseData.intent !== descriptor.intent) fail("AAS_BENCHMARK_INTENT_MISMATCH", caseData.caseId);
|
|
if (caseData.taskFamilyFingerprint !== descriptor.taskFamilyId) {
|
|
fail("AAS_BENCHMARK_FINGERPRINT_MISMATCH", caseData.caseId);
|
|
}
|
|
if (goldData.caseId !== caseData.caseId) fail("AAS_BENCHMARK_PAIR_MISMATCH", caseData.caseId);
|
|
if (!Array.isArray(caseData.criticalGoals) || caseData.criticalGoals.length === 0) {
|
|
fail("AAS_BENCHMARK_CRITICAL_GOALS", caseData.caseId);
|
|
}
|
|
if ((caseData.minimumNonCriticalGoalCoverage ?? 0.8) < 0.8) {
|
|
fail("AAS_BENCHMARK_NONCRITICAL_THRESHOLD", caseData.caseId);
|
|
}
|
|
if (caseData.requiresSkill !== true) fail("AAS_BENCHMARK_REQUIRES_SKILL", caseData.caseId);
|
|
if (!Array.isArray(caseData.targets) || caseData.targets.length === 0) {
|
|
fail("AAS_BENCHMARK_TARGETS", caseData.caseId);
|
|
}
|
|
if (!Array.isArray(caseData.policy?.allowedRisk)
|
|
|| typeof caseData.policy?.requireKnownSource !== "boolean"
|
|
|| typeof caseData.policy?.allowManualSetup !== "boolean") {
|
|
fail("AAS_BENCHMARK_POLICY", caseData.caseId);
|
|
}
|
|
if (!caseData.provenance?.source || !caseData.provenance?.version || !caseData.provenance?.reviewedAt) {
|
|
fail("AAS_BENCHMARK_PROVENANCE", caseData.caseId);
|
|
}
|
|
|
|
for (const solution of goldData.acceptedSolutions || []) {
|
|
const allowed = solution.allowedSkillIds || [];
|
|
if (allowed.length === 0 || new Set(allowed).size !== allowed.length) {
|
|
fail("AAS_BENCHMARK_ALLOWED_SKILLS", `${caseData.caseId}/${solution.solutionId}`);
|
|
}
|
|
for (const skillId of allowed) {
|
|
if (!catalogById.has(skillId)) fail("AAS_BENCHMARK_UNKNOWN_SKILL", `${caseData.caseId}/${skillId}`);
|
|
}
|
|
for (const group of solution.requiredGroups || []) {
|
|
if (!Array.isArray(group) || group.length === 0 || group.some((skillId) => !allowed.includes(skillId))) {
|
|
fail("AAS_BENCHMARK_REQUIRED_GROUP", `${caseData.caseId}/${solution.solutionId}`);
|
|
}
|
|
}
|
|
}
|
|
if (!goldData.provenance?.source || !goldData.provenance?.version || !goldData.provenance?.reviewedAt) {
|
|
fail("AAS_BENCHMARK_GOLD_PROVENANCE", caseData.caseId);
|
|
}
|
|
const solutionFingerprints = new Set();
|
|
for (const solution of goldData.acceptedSolutions || []) {
|
|
const fingerprint = JSON.stringify(canonicalize({
|
|
allowedSkillIds: solution.allowedSkillIds,
|
|
requiredGroups: solution.requiredGroups,
|
|
}));
|
|
if (solutionFingerprints.has(fingerprint)) {
|
|
fail("AAS_BENCHMARK_DUPLICATE_SOLUTION", `${caseData.caseId}/${solution.solutionId}`);
|
|
}
|
|
solutionFingerprints.add(fingerprint);
|
|
for (const skillId of solution.allowedSkillIds || []) {
|
|
const skill = catalogById.get(skillId);
|
|
if (!skill) continue;
|
|
const explicitRisk = skill.risk;
|
|
if (explicitRisk && explicitRisk !== "unknown" && !caseData.policy.allowedRisk.includes(explicitRisk)) {
|
|
fail("AAS_BENCHMARK_GOLD_RISK_VIOLATION", `${caseData.caseId}/${skillId}/${explicitRisk}`);
|
|
}
|
|
if (skill.plugin?.setup?.type === "manual" && caseData.policy.allowManualSetup !== true) {
|
|
fail("AAS_BENCHMARK_GOLD_SETUP_VIOLATION", `${caseData.caseId}/${skillId}`);
|
|
}
|
|
for (const target of caseData.targets) {
|
|
if (skill.plugin?.targets?.[target.host] === "blocked") {
|
|
fail("AAS_BENCHMARK_GOLD_HOST_VIOLATION", `${caseData.caseId}/${skillId}/${target.host}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const digest = pairDigest(caseData, goldData);
|
|
const approvedReviews = (goldData.reviews || []).filter((review) => (
|
|
review.decision === "approved" && review.reviewedDigest === digest
|
|
));
|
|
if (frozen) {
|
|
const uniqueReviewers = new Set(approvedReviews.map((review) => review.reviewer));
|
|
if (uniqueReviewers.size < 2) fail("AAS_BENCHMARK_REVIEW_COUNT", caseData.caseId);
|
|
}
|
|
}
|
|
|
|
for (const descriptor of index.cases) {
|
|
if (!seenIds.has(descriptor.caseId)) fail("AAS_BENCHMARK_MISSING_CASE", descriptor.caseId);
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(JSON.stringify({ ok: false, frozen, failures }, null, 2));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
frozen,
|
|
cases: caseFiles.length,
|
|
gold: goldFiles.length,
|
|
uniqueFingerprints: seenFingerprints.size,
|
|
catalogSkills: catalogById.size,
|
|
}, null, 2));
|