const SHA256 = /^[a-f0-9]{64}$/i; function resourceKey(value) { return String(value ?? "").replaceAll("/", "\\").toLowerCase(); } function requireObject(value, label) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`${label} must be an object.`); } return value; } export function buildOriginNativeMaterialProvenance({ metadata, assembly, semantics, conversionReports }) { requireObject(metadata, "Import metadata"); requireObject(assembly, "Assembly evidence"); requireObject(semantics, "Native semantics report"); if (!Array.isArray(assembly.stagedOverrides) || assembly.stagedOverrides.length < 1) { throw new Error("Assembly evidence must contain one or more staged overrides."); } if (!Array.isArray(assembly.resources)) { throw new Error("Assembly evidence is missing its authoritative source resource rows."); } if (!Array.isArray(semantics.khrMaterialsUnlit) || !Array.isArray(semantics.nativeAdditiveMaterials)) { throw new Error("Native semantics report is missing its verified unlit/additive material lists."); } if (!SHA256.test(assembly.sourceWdbSha256 ?? "") || !SHA256.test(assembly.assembledVisual?.sha256 ?? "")) { throw new Error("Assembly evidence is missing a valid source WDB or assembled-source SHA-256."); } if (assembly.sourceWdbReportSha256 != null && !SHA256.test(assembly.sourceWdbReportSha256)) { throw new Error("Assembly evidence has an invalid source WDB report SHA-256."); } const reportEntries = conversionReports instanceof Map ? conversionReports : new Map(Object.entries(conversionReports ?? {}).map(([key, value]) => [resourceKey(key), value])); const nativeAlphaResources = []; const metadataResources = []; const seenResources = new Set(); const authoritativeHashes = new Map(); for (const override of assembly.stagedOverrides) { const key = resourceKey(override.resource); if (!key || seenResources.has(key)) throw new Error(`Staged override resource is missing or duplicated: ${override.resource}.`); seenResources.add(key); const sourceRows = assembly.resources.filter((row) => resourceKey(row.resource) === key); if (sourceRows.length !== 1) throw new Error(`Expected one authoritative assembly resource row for ${override.resource}; found ${sourceRows.length}.`); const sourceRow = sourceRows[0]; if (!SHA256.test(sourceRow.sourceSha256 ?? "")) throw new Error(`Invalid assembly source SHA-256 for ${override.resource}.`); if (Number(sourceRow.placementCount) !== Number(override.placements)) { throw new Error(`Placement count mismatch for ${override.resource}.`); } const conversion = reportEntries.get(key); const nativeAlpha = conversion?.visual?.materials?.nativeAlpha; if (!nativeAlpha || !Array.isArray(nativeAlpha.materials)) { throw new Error(`Missing native alpha material evidence for ${override.resource}.`); } const sourceSha256 = nativeAlpha.sourceResourceSha256; if (!SHA256.test(sourceSha256 ?? "")) { throw new Error(`Invalid native source hash for ${override.resource}.`); } if (resourceKey(nativeAlpha.sourceModel) !== key) { throw new Error(`Source model identity mismatch for ${override.resource}: ${nativeAlpha.sourceModel}.`); } if (resourceKey(conversion.source?.model) !== key || conversion.source?.resourceSha256 !== sourceSha256) { throw new Error(`Conversion source provenance mismatch for ${override.resource}.`); } if (sourceRow.sourceSha256 !== sourceSha256) { throw new Error(`Conversion source SHA-256 does not match the authoritative assembly row for ${override.resource}.`); } if (nativeAlpha.materials.some((material) => resourceKey(material.sourceModel) !== key || material.sourceResourceSha256 !== sourceSha256)) { throw new Error(`Material-level native identity does not match ${override.resource}.`); } authoritativeHashes.set(key, sourceSha256); nativeAlphaResources.push({ resource: override.resource, placements: override.placements, sourceResourceSha256: sourceSha256, nativeAlpha, }); metadataResources.push({ resource: override.resource, placements: override.placements, sourceResourceSha256: sourceSha256, }); } for (const semanticMaterial of [...semantics.khrMaterialsUnlit, ...semantics.nativeAdditiveMaterials]) { const key = resourceKey(semanticMaterial.sourceModel); if (!key || !authoritativeHashes.has(key) || semanticMaterial.sourceResourceSha256 !== authoritativeHashes.get(key)) { throw new Error(`Unlit/additive material has no exact staged source identity: ${semanticMaterial.sourceModel ?? ""}.`); } } const assemblyProvenance = { sourceWdbSha256: assembly.sourceWdbSha256, sourceWdbReportSha256: assembly.sourceWdbReportSha256 ?? null, assembledSourceSha256: assembly.assembledVisual.sha256, staticPlacements: assembly.staticPlacementCount, uniqueResources: assembly.uniqueResourceCount, dynamicPlacements: assembly.dynamicPlacementCount, stagedOverrideCount: assembly.stagedOverrides.length, primaryResource: assembly.primaryResource, primarySourceSha256: assembly.primarySourceSha256, }; const alphaReport = { schemaVersion: 2, source: "native material conversion reports joined by exact ROS resource identity and source SHA-256", assembly: assemblyProvenance, resources: nativeAlphaResources, }; const semanticsReport = { schemaVersion: 1, source: "native material semantics annotated before gltf-transform optimization", assembly: assemblyProvenance, materialCounts: semantics.materialCounts ?? null, sharedMapLightingPair: semantics.sharedMapLightingPair ?? null, khrMaterialsUnlit: semantics.khrMaterialsUnlit, nativeAdditiveMaterials: semantics.nativeAdditiveMaterials, limitation: "This report records source-proven material semantics for the listed staged overrides; it does not certify every surface as visually correct.", }; const updatedMetadata = structuredClone(metadata); updatedMetadata.nativeMaterialAlpha = { report: "native-material-alpha-report.json", resources: metadataResources, }; updatedMetadata.nativeMaterialSemantics = { report: "native-material-semantics-report.json", stagedOverrideCount: assembly.stagedOverrides.length, additiveMaterialCount: semantics.nativeAdditiveMaterials.length, unlitMaterialCount: semantics.khrMaterialsUnlit.length, assembledSourceSha256: assembly.assembledVisual.sha256, }; return { metadata: updatedMetadata, alphaReport, semanticsReport }; }