const idPattern = /^[a-z][a-z0-9-]{0,63}$/u; const fileNamePattern = /^[a-z0-9][a-z0-9-]*\.(avif|jpg|png|svg|webp|mp4|webm|opus|mp3)$/u; const managedFileNamePattern = /^(?:project|social)-[a-z0-9][a-z0-9-]*\.(?:avif|jpg|png|svg|webp|mp4|webm)$/u; const sha256Pattern = /^[a-f0-9]{64}$/u; const assertSafeRecord = (asset) => { if (!asset || typeof asset !== 'object' || Array.isArray(asset) || typeof asset.id !== 'string' || !idPattern.test(asset.id) || typeof asset.fileName !== 'string' || !fileNamePattern.test(asset.fileName) || typeof asset.sha256 !== 'string' || !sha256Pattern.test(asset.sha256)) { throw new Error('ASSET-MERGE-RECORD-001'); } }; export const mergeAssetRecords = (baseAssets, generatedAssets, managedIds) => { if (!Array.isArray(baseAssets) || !Array.isArray(generatedAssets) || !(managedIds instanceof Set)) { throw new Error('ASSET-MERGE-RECORD-001'); } for (const asset of [...baseAssets, ...generatedAssets]) { if (!asset || typeof asset !== 'object' || Array.isArray(asset) || typeof asset.id !== 'string' || typeof asset.fileName !== 'string') { throw new Error('ASSET-MERGE-RECORD-001'); } } const retained = baseAssets.filter(({ id, fileName }) => !managedIds.has(id) || !managedFileNamePattern.test(fileName)); const combined = [...retained, ...generatedAssets]; const ids = new Set(); const names = new Set(); for (const asset of combined) { const folded = asset.fileName.toLocaleLowerCase('en-US'); if (ids.has(asset.id)) throw new Error('ASSET-MERGE-ID-001'); if (names.has(folded)) throw new Error('ASSET-MERGE-NAME-001'); ids.add(asset.id); names.add(folded); } for (const asset of [...baseAssets, ...generatedAssets]) assertSafeRecord(asset); return combined.sort((left, right) => left.fileName.localeCompare(right.fileName, 'en-US')); };