mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-20 23:11:46 +02:00
107 lines
4.4 KiB
JavaScript
107 lines
4.4 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const SCHEMA_VERSION = 1;
|
|
|
|
function sha256(content) {
|
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
}
|
|
|
|
function hashFile(file) {
|
|
return sha256(fs.readFileSync(file));
|
|
}
|
|
|
|
function hashDirectory(dir) {
|
|
const entries = [];
|
|
function visit(current, relative = '') {
|
|
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
const child = path.join(current, entry.name);
|
|
const name = path.posix.join(relative.split(path.sep).join('/'), entry.name);
|
|
if (entry.isDirectory()) visit(child, name);
|
|
else if (entry.isFile()) entries.push({ path: name, sha256: hashFile(child) });
|
|
else throw new Error(`unsupported evidence input: ${child}`);
|
|
}
|
|
}
|
|
visit(dir);
|
|
return sha256(JSON.stringify(entries));
|
|
}
|
|
|
|
function validateRows(rows, trials) {
|
|
const scheduled = new Map(trials.map(trial => [trial.key, trial]));
|
|
const validKeys = new Set();
|
|
for (const row of rows) {
|
|
if (!row || typeof row.key !== 'string' || !scheduled.has(row.key)) {
|
|
throw new Error(`result row has an unknown scheduled key: ${row && row.key}`);
|
|
}
|
|
const trial = scheduled.get(row.key);
|
|
if (row.scenario !== trial.scenario.id || row.arm !== trial.arm || row.rep !== trial.rep) {
|
|
throw new Error(`result row does not match its scheduled key: ${row.key}`);
|
|
}
|
|
if (!row.providerError && !row.timedOut && !row.judgeFailed) {
|
|
if (validKeys.has(row.key)) throw new Error(`duplicate valid result row: ${row.key}`);
|
|
validKeys.add(row.key);
|
|
}
|
|
}
|
|
return validKeys;
|
|
}
|
|
|
|
function buildManifest({ meta, configuration, scenarioFingerprints, sourceFingerprints, outDir, rows = [] }) {
|
|
const files = {};
|
|
for (const name of ['meta.json', 'results.jsonl', 'holes.md', 'summary.json']) {
|
|
const file = path.join(outDir, name);
|
|
if (fs.existsSync(file)) files[name] = { bytes: fs.statSync(file).size, sha256: hashFile(file) };
|
|
}
|
|
const transcriptsDir = path.join(outDir, 'transcripts');
|
|
if (fs.existsSync(transcriptsDir)) {
|
|
for (const name of fs.readdirSync(transcriptsDir).sort()) {
|
|
const file = path.join(transcriptsDir, name);
|
|
if (fs.statSync(file).isFile()) files[`transcripts/${name}`] = { bytes: fs.statSync(file).size, sha256: hashFile(file) };
|
|
}
|
|
}
|
|
return {
|
|
schemaVersion: SCHEMA_VERSION,
|
|
generatedAt: new Date().toISOString(),
|
|
meta,
|
|
configuration,
|
|
scenarioFingerprints,
|
|
sourceFingerprints,
|
|
integrity: { rowTranscriptLinks: rows.length > 0 && rows.every(row => Boolean(row.transcript && row.transcriptSha256)) },
|
|
runtime: { node: process.version, platform: process.platform, arch: process.arch },
|
|
files
|
|
};
|
|
}
|
|
|
|
function verifyManifest(outDir) {
|
|
const manifestPath = path.join(outDir, 'evidence.json');
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
if (manifest.schemaVersion !== SCHEMA_VERSION || !manifest.files || typeof manifest.files !== 'object') {
|
|
throw new Error('unsupported or malformed evidence manifest');
|
|
}
|
|
for (const [name, expected] of Object.entries(manifest.files)) {
|
|
const file = path.resolve(outDir, name);
|
|
if (!file.startsWith(`${path.resolve(outDir)}${path.sep}`)) throw new Error(`invalid evidence path: ${name}`);
|
|
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error(`missing evidence artifact: ${name}`);
|
|
const actual = hashFile(file);
|
|
if (actual !== expected.sha256 || fs.statSync(file).size !== expected.bytes) {
|
|
throw new Error(`evidence artifact checksum mismatch: ${name}`);
|
|
}
|
|
}
|
|
if (manifest.integrity && manifest.integrity.rowTranscriptLinks) {
|
|
const resultFile = path.join(outDir, 'results.jsonl');
|
|
if (!fs.existsSync(resultFile)) throw new Error('results.jsonl is missing from a transcript-linked bundle');
|
|
const rows = fs.readFileSync(resultFile, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
for (const row of rows) {
|
|
const transcript = manifest.files[row.transcript];
|
|
if (!transcript || transcript.sha256 !== row.transcriptSha256) {
|
|
throw new Error(`result transcript link is missing or mismatched: ${row.key}`);
|
|
}
|
|
}
|
|
}
|
|
return manifest;
|
|
}
|
|
|
|
module.exports = { SCHEMA_VERSION, hashFile, hashDirectory, validateRows, buildManifest, verifyManifest };
|