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, }); }