109 lines
5.5 KiB
JavaScript
109 lines
5.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const outputPath = path.join(root, "tests/golden/corrective/rehydration/M12-provenance-intake.json");
|
|
const driftPath = path.join(root, "tests/golden/corrective/rehydration/M12-manifest-sha-drift.json");
|
|
const snapshotArgument = process.argv.indexOf("--snapshot");
|
|
const requestedSnapshot = snapshotArgument >= 0 ? process.argv[snapshotArgument + 1] : null;
|
|
const write = process.argv.includes("--write");
|
|
|
|
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
|
|
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
|
const drift = readJson(driftPath);
|
|
|
|
function emptyReport(reason) {
|
|
return {
|
|
schemaVersion: 1,
|
|
operation: "M12_IMMUTABLE_SNAPSHOT_PROVENANCE_INTAKE",
|
|
historicalEvidencePreserved: true,
|
|
immutableSourceSnapshotAvailable: false,
|
|
provenanceState: "BLOCKED_SOURCE_LOST",
|
|
sourceSnapshot: null,
|
|
sourceSnapshotManifestSha256: null,
|
|
driftEntries: drift.entries.length,
|
|
restoredEntries: 0,
|
|
remainingEntries: drift.entries.length,
|
|
reason,
|
|
entries: drift.entries.map((entry) => ({ ...entry, provenanceState: "BLOCKED_SOURCE_LOST" })),
|
|
};
|
|
}
|
|
|
|
function resolveSnapshot(value) {
|
|
if (!value) return null;
|
|
const candidate = path.resolve(root, value);
|
|
if (!fs.existsSync(candidate)) return null;
|
|
const manifestPath = fs.statSync(candidate).isDirectory() ? path.join(candidate, "snapshot-manifest.json") : candidate;
|
|
if (!fs.existsSync(manifestPath)) return null;
|
|
const manifest = readJson(manifestPath);
|
|
assert.equal(manifest.schemaVersion, 1, "snapshot manifest schema must be 1");
|
|
assert.ok(["IMMUTABLE_ARCHIVE", "IMMUTABLE_REPOSITORY", "SIGNED_ARTIFACT"].includes(manifest.mode), "snapshot mode is not immutable");
|
|
assert.equal(manifest.authorized, true, "snapshot must be authorized");
|
|
assert.ok(Array.isArray(manifest.files), "snapshot manifest files are missing");
|
|
const rootPath = path.dirname(manifestPath);
|
|
const files = new Map();
|
|
for (const entry of manifest.files) {
|
|
assert.equal(typeof entry.path, "string");
|
|
assert.match(entry.sha256 ?? "", /^[a-f0-9]{64}$/u);
|
|
const sourcePath = path.resolve(rootPath, entry.snapshotPath ?? entry.path);
|
|
assert.ok(sourcePath === rootPath || sourcePath.startsWith(`${rootPath}${path.sep}`), `snapshot path escapes root: ${entry.path}`);
|
|
assert.ok(fs.existsSync(sourcePath), `snapshot file is missing: ${entry.path}`);
|
|
assert.equal(sha256(sourcePath), entry.sha256, `snapshot hash drift: ${entry.path}`);
|
|
if (entry.bytes !== undefined) assert.equal(fs.statSync(sourcePath).size, entry.bytes, `snapshot byte drift: ${entry.path}`);
|
|
const key = `${entry.path}\0${entry.sha256}`;
|
|
if (files.has(key)) throw new Error(`duplicate snapshot path/SHA pair: ${entry.path}`);
|
|
files.set(key, { ...entry, sourcePath });
|
|
}
|
|
return { rootPath, manifestPath, manifest, files };
|
|
}
|
|
|
|
let report;
|
|
if (!requestedSnapshot) {
|
|
report = emptyReport("immutable snapshot was not supplied; no historical source may be inferred from the current worktree");
|
|
} else {
|
|
const snapshot = resolveSnapshot(requestedSnapshot);
|
|
if (!snapshot) {
|
|
report = emptyReport(`immutable snapshot is unavailable or invalid: ${requestedSnapshot}`);
|
|
} else {
|
|
const restored = [];
|
|
const remaining = [];
|
|
for (const entry of drift.entries) {
|
|
const source = snapshot.files.get(`${entry.path}\0${entry.originalSha256}`);
|
|
if (source) {
|
|
restored.push({ ...entry, snapshotPath: relative(source.sourcePath), provenanceState: "PROVENANCE_RESTORED" });
|
|
} else {
|
|
remaining.push({ ...entry, snapshotPath: source ? relative(source.sourcePath) : null, provenanceState: "BLOCKED_SOURCE_LOST" });
|
|
}
|
|
}
|
|
report = {
|
|
schemaVersion: 1,
|
|
operation: "M12_IMMUTABLE_SNAPSHOT_PROVENANCE_INTAKE",
|
|
historicalEvidencePreserved: true,
|
|
immutableSourceSnapshotAvailable: true,
|
|
provenanceState: remaining.length === 0 ? "PROVENANCE_RESTORED" : "BLOCKED_SOURCE_LOST",
|
|
sourceSnapshot: relative(snapshot.rootPath),
|
|
sourceSnapshotManifestSha256: sha256(snapshot.manifestPath),
|
|
snapshotMode: snapshot.manifest.mode,
|
|
driftEntries: drift.entries.length,
|
|
restoredEntries: restored.length,
|
|
remainingEntries: remaining.length,
|
|
reason: remaining.length === 0 ? "all observed M12 drift entries match the immutable source snapshot" : "snapshot supplied but some historical hashes remain unresolved",
|
|
entries: [...restored, ...remaining].sort((a, b) => `${a.task}:${a.artifact}:${a.path}`.localeCompare(`${b.task}:${b.artifact}:${b.path}`)),
|
|
};
|
|
}
|
|
}
|
|
|
|
if (write) {
|
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
} else {
|
|
assert.ok(fs.existsSync(outputPath), "provenance intake report is missing; run with --write");
|
|
assert.deepEqual(readJson(outputPath), report, "provenance intake report is stale or source changed");
|
|
}
|
|
|
|
process.stdout.write(`m12-provenance-intake-ok state=${report.provenanceState} drift=${report.driftEntries} restored=${report.restoredEntries} remaining=${report.remainingEntries} snapshot=${report.immutableSourceSnapshotAvailable} write=${write}\n`);
|