/** * 原生导入记录到纯ImportUnit的转换:源ID经reserve映射为新身份,依赖保留为source key等待闭合。 * $target或文档源根映射到显式选定的现有canvas;副本incarnation从1开始,不继承旧墓碑/任务执行。 * 本模块只规划changes,不写文档、不上传/授权;资源地址和授权事实由宿主/engine另行提供与校验。 */ import type { Document, ElementRecord, Id, Json, Registry } from '../canvas-core/types.ts'; import { copyReferenceIds } from '../canvas-core/copy-paths.ts'; import { remapCopyData } from '../canvas-core/duplicate.ts'; import { clone, fault } from '../canvas-core/values.ts'; import type { ImportJob, ImportLine, ImportUnit } from './imports.ts'; /** 校验目标canvas仍存在且入口ID/代次或根身份未变,防旧任务把尾批导入后来替换的分区。 */ export function assertNativeImportTarget(job:ImportJob,document:Document){ const source=job.source,canvas=source?document.canvases.get(source.targetCanvasId):undefined; if(!source||!canvas)throw fault('import_target_missing'); if(source.targetEntry?(canvas.parent?.entryElementId!==source.targetEntry.id||document.elements.get(source.targetEntry.id)?.incarnation!==source.targetEntry.incarnation):canvas.parent!==null)throw fault('import_target_changed'); return source; } /** 已committed依赖必须仍是原绑定生命周期;用户删后重建同ID不能被后续导入误认领。 */ export function assertNativeImportDependencies(job:ImportJob,document:Document,dependencies:readonly string[]){ for(const key of dependencies)if(job.committedSourceKeys.includes(key)){ const id=job.stableIds[key],binding=job.bindings?.[id]; if(!binding)throw fault('import_dependency_changed'); if(binding.kind==='element'){if(document.elements.get(id)?.incarnation!==binding.incarnation)throw fault('import_dependency_changed',[id]);} else{const canvas=document.canvases.get(id),entry=binding.entryElementId,entryBinding=entry?job.bindings?.[entry]:undefined;if(!canvas||canvas.parent?.entryElementId!==entry||entry&&(entryBinding?.kind!=='element'||document.elements.get(entry)?.incarnation!==entryBinding.incarnation))throw fault('import_dependency_changed',[id]);} } } /** Native NDJSON records use source IDs; $target names the explicitly chosen existing canvas. */ /** 校验记录封装/源ID,预留自身与引用ID,生成一条创建变化及去除自引用后的依赖列表。 */ export function prepareNativeImport(line:ImportLine,job:ImportJob,reserve:(key:string)=>Id,document:Document,registry:Registry,resourceURL?:(id:Id)=>string):ImportUnit { const source=assertNativeImportTarget(job,document); const raw=line.value as Record|undefined; if(!raw||Array.isArray(raw)||!['element','canvas'].includes(raw.kind as string)||typeof raw.key!=='string'||!raw.key||raw.key==='$target'||!raw.record||typeof raw.record!=='object'||Array.isArray(raw.record))throw fault('invalid_native_import_record'); const record=clone(raw.record) as Record;if(record.id!==raw.key)throw fault('import_source_identity_mismatch'); const dependencies=new Set(),elementIds=new Map(),canvasIds=new Map(); /** 元素引用先登记依赖再reserve,可先遇到引用后遇到定义,恢复仍复用同一个新ID。 */ const element=(id:Json):Id=>{if(typeof id!=='string'||!id)throw fault('invalid_import_reference');const key=`element:${id}`;dependencies.add(key);const mapped=reserve(key);elementIds.set(id,mapped);return mapped;}; /** 根/$target直接重定向目标,子canvas则分配稳定新ID并登记canvas依赖。 */ const owner=(id:Json):Id=>{if(id==='$target'||source.kind==='native-document'&&id===raw.sourceRootId){canvasIds.set(id as string,source.targetCanvasId);return source.targetCanvasId;}if(typeof id!=='string'||!id)throw fault('invalid_import_reference');const key=`canvas:${id}`;dependencies.add(key);const mapped=reserve(key);canvasIds.set(id,mapped);return mapped;}; const key=`${raw.kind}:${raw.key}`,id=reserve(key); // 不把源根作为第二个根插入;这里只处理有parent的子canvas,parent与入口都使用新映射。 if(raw.kind==='canvas'){ const parent=record.parent as Record|null; if(!parent||typeof parent!=='object'||Array.isArray(parent))throw fault('invalid_import_canvas'); const after={...record,id,parent:{...parent,canvasId:owner(parent.canvasId),entryElementId:element(parent.entryElementId)}}; dependencies.delete(key);return{key,dependencies:[...dependencies],changes:[{kind:'canvas',id,before:null,after:after as never}]}; } // record是脱离源的副本,original是其类型视图;未知组件/缺复制声明不猜字段含义。 const original=record as unknown as ElementRecord,definition=registry.components.get(original.componentId); if(registry.resolve(original)!=='known'||!definition?.copy||!original.data||typeof original.data!=='object'||Array.isArray(original.data))throw fault('import_component_unavailable'); // 文档图片需宿主提供规范资源URL;仅重建引用不代表完成授权,授权由运行时后续门禁证明。 if(source.kind==='native-document')for(const key of definition.resourceFields??[]){const value=original.data[key];if(value===undefined||value===null)continue;if(!resourceURL)throw fault('import_resource_authorization_required');if(typeof value!=='string'||!value)throw fault('invalid_import_reference');if(key==='assetId')record.data={...original.data,src:resourceURL(value)};} record.data=original.data; // 只遍历schema声明引用,避免重写普通文字里的ID;omit字段由共享remapCopyData处理。 for(const ref of definition.copy.references)for(const item of copyReferenceIds(original.data,ref)){if(ref.target==='element')element(item);else owner(item);} const after={...record,id,incarnation:1,ownerCanvasId:owner(original.ownerCanvasId),data:remapCopyData(original,registry,elementIds,canvasIds,document)}; // 连线端点同样进入依赖/映射;不能以源owner或字符串形似就猜端点关系。 if(original.structure==='connection'){ if(!original.source||!original.target)throw fault('invalid_import_endpoint'); Object.assign(after,{source:{...original.source,elementId:element(original.source.elementId)},target:{...original.target,elementId:element(original.target.elementId)}}); }else if(original.structure!=='node')throw fault('invalid_import_structure'); dependencies.delete(key);return{key,dependencies:[...dependencies],changes:[{kind:'element',id,before:null,after:after as ElementRecord}]}; }