Files
healer-man/scripts/runewaker-pipeline/lib/native-core-candidate-provenance.mjs
T
2026-10-06 22:25:34 -04:00

125 lines
5.9 KiB
JavaScript

const normalizeResource = (value) => String(value ?? "")
.replaceAll("\\", "/")
.replace(/^\/+/, "")
.toLowerCase();
/**
* The Windows native audit tool currently emits its input/output path strings
* directly into JSON. Forward slashes are accepted by the filesystem and
* prevent unescaped backslashes from making that report invalid JSON.
*/
export function buildNativeCollisionAuditArguments({ resourceRoot, sourceModel, reportPath }) {
const normalizePath = (value, label) => {
const normalized = String(value ?? "").replaceAll("\\", "/");
if (!normalized) throw new Error(`${label} is required for native collision audit.`);
return normalized;
};
return [
normalizePath(resourceRoot, "Resource root"),
normalizePath(sourceModel, "Source ROS path"),
normalizePath(reportPath, "Native report path"),
"respect-options",
];
}
function requireHash(value, label) {
const hash = String(value ?? "").toLowerCase();
if (!/^[a-f0-9]{64}$/.test(hash)) throw new Error(`${label} must be a SHA-256 hash.`);
return hash;
}
function requireEqual(actual, expected, label) {
if (actual !== expected) throw new Error(`${label} does not match.`);
}
function samePlacement(actual, expected) {
if (!actual || !expected) return false;
if (String(actual.guid ?? "").toLowerCase() !== String(expected.guid ?? "").toLowerCase()) return false;
if (normalizeResource(actual.resource) !== normalizeResource(expected.resource)) return false;
for (const field of ["translation", "rotation", "scale"]) {
if (!Array.isArray(actual[field]) || !Array.isArray(expected[field]) || actual[field].length !== expected[field].length) return false;
for (let index = 0; index < actual[field].length; index += 1) {
if (Math.abs(Number(actual[field][index]) - Number(expected[field][index])) > 1e-5) return false;
}
}
return true;
}
/**
* Verifies that native-core collision candidates are bound to one exact
* recipe, primary ROS/WDB source cache, import metadata, WDB placement, and
* strict engine-query geometry report before any candidate files are emitted.
*/
export function validateNativeCoreCandidateProvenance(input) {
const {
slug,
recipe,
recipeSha256,
importMetadata,
actualSourceModelSha256,
actualSourceWdbSha256,
wdbReport,
wdbReportSha256,
exportReport,
exportReportSha256,
conversionReportSha256,
nativeCollisionReport,
nativeCollisionReportSha256,
nativeCollisionObjSha256,
nativeCollisionFlagsSha256,
primaryPlacement,
} = input;
requireEqual(recipe?.slug, slug, "Native-core candidate recipe slug");
requireEqual(importMetadata?.dungeonId, slug, "Native-core import metadata dungeon ID");
const recipeModel = normalizeResource(recipe?.source?.model);
const recipeWdb = normalizeResource(recipe?.source?.wdb);
requireEqual(normalizeResource(importMetadata.source?.model), recipeModel, "Imported source ROS path");
requireEqual(normalizeResource(importMetadata.source?.wdb), recipeWdb, "Imported source WDB path");
const sourceModelSha256 = requireHash(actualSourceModelSha256, "Current source ROS SHA-256");
const sourceWdbSha256 = requireHash(actualSourceWdbSha256, "Current source WDB SHA-256");
requireEqual(requireHash(importMetadata.source?.modelSha256, "Imported source ROS SHA-256"), sourceModelSha256, "Imported source ROS hash");
requireEqual(requireHash(importMetadata.source?.wdbSha256, "Imported source WDB SHA-256"), sourceWdbSha256, "Imported source WDB hash");
const recipeHash = requireHash(recipeSha256, "Recipe file SHA-256");
const wdbReportHash = requireHash(wdbReportSha256, "WDB descriptor report SHA-256");
const exportReportHash = requireHash(exportReportSha256, "Native export report SHA-256");
const conversionHash = requireHash(conversionReportSha256, "Converted collision report SHA-256");
const nativeReportHash = requireHash(nativeCollisionReportSha256, "Native collision query report SHA-256");
const nativeObjHash = requireHash(nativeCollisionObjSha256, "Native collision query OBJ SHA-256");
const nativeFlagsHash = requireHash(nativeCollisionFlagsSha256, "Native collision query flags SHA-256");
const primaryDescriptor = wdbReport?.descriptors?.find((descriptor) => samePlacement(descriptor, primaryPlacement));
if (!primaryDescriptor || normalizeResource(primaryDescriptor.resource) !== recipeModel) {
throw new Error("Native-core primary WDB descriptor does not match the resolved placement and recipe ROS.");
}
requireEqual(normalizeResource(exportReport?.sourceModel), recipeModel, "Native exporter source ROS path");
requireEqual(normalizeResource(nativeCollisionReport?.sourceModel), recipeModel, "Native collision query source ROS path");
requireEqual(nativeCollisionReport?.collisionMode, "respect-options", "Native collision query mode");
requireEqual(nativeCollisionReport?.queryMasks?.ignoreEntityCollisionOption, false, "Native collision entity-option gate policy");
requireEqual(nativeCollisionReport?.renderMeshFallbackTriangleCount, 0, "Native collision render-mesh fallback count");
if (
!Number.isInteger(nativeCollisionReport?.nativeCollisionTriangleCount)
|| nativeCollisionReport.nativeCollisionTriangleCount <= 0
|| nativeCollisionReport.nativeCollisionTriangleCount !== nativeCollisionReport.enabledNativeCollisionTriangleCount
) {
throw new Error("Options-aware collision query count must equal the enabled serialized native triangle count.");
}
return Object.freeze({
recipeSha256: recipeHash,
sourceModelSha256,
sourceWdbSha256,
wdbReportSha256: wdbReportHash,
exportReportSha256: exportReportHash,
conversionReportSha256: conversionHash,
nativeCollisionReportSha256: nativeReportHash,
nativeCollisionObjSha256: nativeObjHash,
nativeCollisionFlagsSha256: nativeFlagsHash,
nativeEnabledTriangleCount: nativeCollisionReport.nativeCollisionTriangleCount,
primaryPlacementGuid: primaryDescriptor.guid,
});
}