From 59b59c2f0305ceab51121ceafdb5c99d8cf64bd4 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 13:39:25 +0800 Subject: [PATCH] docs(runtime): clarify partial loading structure and import gates --- src/lib/canvas-runtime/loading-import.ts | 14 +++++++-- src/lib/canvas-runtime/loading-structure.ts | 34 +++++++++++++++------ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/lib/canvas-runtime/loading-import.ts b/src/lib/canvas-runtime/loading-import.ts index d1e8539..bac3c05 100644 --- a/src/lib/canvas-runtime/loading-import.ts +++ b/src/lib/canvas-runtime/loading-import.ts @@ -16,7 +16,10 @@ const relations=new WeakMap;children:Map /** 构造直接成员和entry→子canvas映射,供选区DFS扩张使用,不从节点可见性推断拥有范围。 */ function index(document:Document){let value=relations.get(document);if(!value){const members=new Map(),children=new Map();for(const element of document.elements.values()){const ids=members.get(element.ownerCanvasId)??[];ids.push(element.id);members.set(element.ownerCanvasId,ids);}for(const canvas of document.canvases.values())if(canvas.parent){const ids=children.get(canvas.parent.entryElementId)??[];ids.push(canvas.id);children.set(canvas.parent.entryElementId,ids);}value={members,children};relations.set(document,value);}return value;} /** The caller captures this proof with the same document that becomes source bytes. */ -/** target先闭合;复制还要求全部选中后代/必要祖先/上级入边owner完整,然后冻结准入范围与sequence。 */ +/** 无 loading 返回 undefined;只有 v5/v6+loadingRead 能生成准入,target 必须 ownerReady。 + * selection 存在时以数组栈+copied Set 深扩张,补入头部声明却未发布的 child,避免误将未读分区当叶子。 + * captured 元素还检查必要祖先;上级入边的 owner 也纳入 required,最终冻结去重排序范围与账本最大 sequence。 + * 只派生证据,不复制字节/分配 ID/验证 policy;没有 selection 表示普通目标导入,不检查复制子树。 */ export function loadingImportAdmission(snapshot:EngineSnapshot,target:Id,selection?:readonly Target[]):LoadingImportAdmission|undefined { const state=snapshot.runtime.loading;if(!state)return; if((state.version!==5&&state.version!==6)||!snapshot.runtime.loadingRead)throw fault('model_loading'); @@ -42,7 +45,9 @@ export function loadingImportAdmission(snapshot:EngineSnapshot,target:Id,selecti let operationSequence=0;for(const record of snapshot.runtime.ledger.operations.values())operationSequence=Math.max(operationSequence,record.intent.sequence); return freeze({version:1 as const,sourceToken:state.sourceToken,operationSequence,requiredCanvasIds:[...required].sort()}); } -/** 恢复时job须同项目/同token,证明sequence不超账本,target与全部必要canvas仍是可证明范围。 */ +/** v5/v6 恢复/发布时逐个 validateImportJob,再核对项目、token、sequence 上界、target 包含与各范围准入。 + * 只检查保存的 requiredCanvasIds,不在此重新展开 selection 或读取源字节;复制源重建由下一函数负责。 + * 依赖外围已建立 loadingRead/账本身份,本函数不证明原 checkpoint 已落盘或获得当前服务器权限。 */ export function validateLoadingImportAdmissions(runtime:EngineSnapshot['runtime']):void { const state=runtime.loading;if(!state||(state.version!==5&&state.version!==6))return; let maxSequence=0;for(const record of runtime.ledger.operations.values())maxSequence=Math.max(maxSequence,record.intent.sequence); @@ -51,7 +56,10 @@ export function validateLoadingImportAdmissions(runtime:EngineSnapshot['runtime' /** Revalidate the frozen copy topology before planning or resuming its source. * Later source chunks may add unrelated owners, so compare only the closed cut. */ -/** 恢复规划前按准入sequence重建预期源,只比较原闭合cut的topology,不要求后到无关owner保持不增长。 */ +/** 从原捕获源重放 sequence≤准入时点的全部意图 changes(不按 phase 过滤,conditional=false)重建旧逻辑切点。 + * 用 preparation/plan 中 selection 重算 required 范围并逐 owner 比较 canvas 与 copyTopology,忽略无关后到范围。 + * copyTopology 保留身份/owner/端点/几何及表现尺度,省略正文等组件内容和边 path;这不是完整 payload 一致性证明。 + * 不计算 SHA、不执行完整文档/权限校验,也不发布重建文档;源字节完整性由快照/导入证据链负责。 */ export function validateLoadingCopySource(snapshot:EngineSnapshot,job:ImportJob,document:Document):void { const state=snapshot.runtime.loading,proof=job.source?.loadingAdmission;if(!state||(state.version!==5&&state.version!==6)||!proof)return; const source=snapshot.runtime.loadingRead!;let expected=source.document; diff --git a/src/lib/canvas-runtime/loading-structure.ts b/src/lib/canvas-runtime/loading-structure.ts index 6c8e73a..5480539 100644 --- a/src/lib/canvas-runtime/loading-structure.ts +++ b/src/lib/canvas-runtime/loading-structure.ts @@ -17,11 +17,16 @@ const counts=new WeakMap>(); function membership(document:Document){let result=counts.get(document);if(!result){const next=new Map();for(const element of document.elements.values())next.set(element.ownerCanvasId,(next.get(element.ownerCanvasId)??0)+1);result=next;counts.set(document,result);}return result;} /** 全部声明元素/canvas 均到齐才算源闭合;这里只做数量闭合判断,领域合法性由其它校验共同保证。 */ export function sourceClosed(state:LoadingState,source:Capture){return source.document.elements.size===state.totalElements&&source.document.canvases.size===state.declaredCanvasIds.length;} -/** working中owner存在,且是明确新canvas或源成员数已达头部证明,才能作为闭合操作范围。 */ +/** working 中 canvas 必须存在;source.versions 不含它时按本地新 scope 处理,否则需 v2+ 直属源计数齐。 + * 依赖 source/state 事先验证的完整声明,不以 working 乐观新增数补齐;也不单独证明全部后代/引用闭合。 */ export function ownerReady(state:LoadingState,source:Capture,document:Document,id:Id){return document.canvases.has(id)&&(!source.versions.has(id)||state.version!==1&&(membership(source.document).get(id)??0)===state.canvasElementCounts[id]);} -/** 只有v4+完整ID命名空间才能证明一个ID不属于原捕获;“当前还没看到”本身不够。 */ +/** 仅 v4/v5/v6 用完整 elementOwners 的缺席判定不属于捕获,不能只看当前 prefix 未出现。 + * 这里不查元素是否真的新建、墓碑或 ID 冲突;born 是命名空间分类,具体创建条件在其它分支核对。 */ function born(state:LoadingState,id:Id){return (state.version===4||state.version===5||state.version===6)&&!Object.hasOwn(state.elementOwners,id);} -/** 禁止凭早期payload引入新任务状态或任意资源URL;新资源须有准入表,已有资源须能在已知数据证明。 */ +/** 数据门禁要求 copy 声明:ai/taskId/runId 的非空值只能与 prior 相等,允许清空而不允许改成新任务值。 + * src/assetId/声明资源字段若非空,须命中受控 assets 表,或与 working/source 已知组件的同字段值一致。 + * 这是可解释来源检查,不发授权请求、不验证 URL 协议,也不代替数据 schema/任务服务校验。 + * 比对可扫描当前与源的元素表,不能把此函数当作常数成本字段检查。 */ function safeData(data:ElementRecord['data'],registry:Registry,component:Id,document:Document,source?:Document,prior?:ElementRecord['data'],assets?:ReadonlyMap){ const definition=registry.components.get(component);if(!definition?.copy)return false; for(const key of ['ai','taskId','runId'])if(data[key]!==undefined&&data[key]!==null&&!equal(data[key],prior?.[key]))return false; @@ -38,7 +43,10 @@ function assetPatch(patch:ElementRecord['data'],registry:Registry,component:Id){ const definition=registry.components.get(component),fields=['src','assetId',...(definition?.resourceFields??[])],keys=Object.keys(patch); return keys.some(key=>fields.includes(key))&&keys.every(key=>fields.includes(key)||!['presentation','ports','ai','taskId','runId'].includes(key)&&!definition?.copy?.references.some(ref=>ref.path[0]===key||ref.path[0]==='*')); } -/** 返回结构命令的预检结果,undefined交普通字段门;准入仍需后续核心policy/schema与实际changes检查。 */ +/** v4+ 对结构/受控数据命令返回 enabled/deny;未处理命令或不满足专用数据入口时返回 undefined。 + * undefined 留给普通字段门,不表示允许;enabled 也不证明目标代次、能力、handler 副作用与最终文档都合法。 + * 原捕获对象的删除/重挂等要求源闭合,本地 born 对象可在 ready owner 操作;解散仍要求整个源到齐。 + * 函数只预检,不锁范围、不生成 changes;调用方仍需核心 policy/schema 和 assertStructuralChanges。 */ export function structuralCommand(command:Command,document:Document,registry:Registry,state:LoadingState,source:Capture,assets?:ReadonlyMap):Availability|undefined { if(state.version<4)return; const deny:Availability={enabled:false,code:'model_loading',targets:[]},yes:Availability={enabled:true}; @@ -52,7 +60,10 @@ export function structuralCommand(command:Command,document:Document,registry:Reg if(command.type==='element.data'&&(born(state,command.target.id)||sourceClosed(state,source)||state.version===6&&assetPatch(command.patch,registry,document.elements.get(command.target.id)?.componentId??'')&&ownerReady(state,source,document,document.elements.get(command.target.id)?.ownerCanvasId??''))){const element=document.elements.get(command.target.id);return element&®istry.resolve(element)==='known'&&safeData({...element.data,...command.patch},registry,element.componentId,document,source.document,element.data,assets)?yes:deny;} } /** Actual changes, not a command name, establish the mutation's required closure. */ -/** 逐条在私有cursor验证实际变化的必要闭合范围/数据,并检查最终声明引用,防handler副作用绕过。 */ +/** 将每条实际 change 按顺序作用到私有 cursor,逐项核对 owner/生命周期分类/数据门禁,最终检查声明引用。 + * geometry 的局部门只要求已知 node,path/端点还要求 ready owner;owner 迁移则检查新旧两侧与 captured 分类。 + * applyChanges 检查 before 条件,但这里不调用完整 validateDocument,也不执行 capability/policy/服务器授权校验。 + * 返回是通过加载门禁的候选,非已发布 Root;任何抛错都不返回半份候选,原文档按不可变输入合同复用。 */ export function assertStructuralChanges(changes:readonly Change[],document:Document,registry:Registry,state:LoadingState,source:Capture,assets?:ReadonlyMap):Document { if(state.version<4)throw fault('model_loading'); // 按变化顺序推进,同批先创建owner/端点可满足后续依赖,但原源命名空间不会因此被改写。 @@ -84,10 +95,13 @@ export function assertStructuralChanges(changes:readonly Change[],document:Docum // 即使命令名字获准,最终仍不能产生悬空声明引用;整批失败不给调用方半份合法结果。 references(cursor,registry);return cursor; } -/** Protocol-2 receipts contain exactly these applied logical changes. */ +/** The caller's validated protocol-2 ledger supplies the applied logical effects. */ // 缓存同一源上的已确认结构投影,只在applied意图前缀身份未变时从缓存后缀继续。 const projectionCache=new WeakMap(); -/** 将resolved/applied意图按sequence叠到读取候选,并按精确协议2的参与者版本推进;不混入pending。 */ +/** 按 sequence 对 resolved+resolution=applied 意图叠原 changes,conditional=false,不加入 pending/elided 效果。 + * 不按 operationType 额外过滤,也不再次核验协议/回执或调用 validateDocument;依赖外围已验证账本与精确协议 2 合同。 + * 文档缓存仅在同 source 与 intent 引用前缀一致时续算;参与者版本另按每个 applied request 的 expected+1 取最大值。 + * 返回新的投影 document/versions,不修改 runtime ACK;最终合法性由加载合并/验证入口继续检查。 */ export function appliedSource(runtime:RuntimeState,source:Capture):Capture { let document=source.document;const versions=new Map(source.versions); const applied=[...runtime.ledger.operations.values()].filter(record=>record.phase==='resolved'&&record.resolution==='applied').sort((a,b)=>a.intent.sequence-b.intent.sequence); @@ -98,9 +112,11 @@ export function appliedSource(runtime:RuntimeState,source:Capture):Capture { return{document,versions}; } -// source/registry/intent/before/assets全身份绑定校验缓存,不能把另一捕获或不同资源授权的结果拿来复用。 +// 缓存键为 source/registry/intent 引用,再比较 before/assets 引用;不按内容 hash 或 LoadingState 单独建键。 const intentProofs=new WeakMap}>>>(); -/** 对同一候选上下文复用已验证结构结果,否则重新assert全部实际changes;不是跳过当前源范围验证。 */ +/** 命中同一 source/registry/intent/before/assets 上下文时直接复用候选,否则重新检查 changes 并缓存。 + * LoadingState 不在独立缓存键内,调用方须保证其与同源上下文一致,所有参与引用均按不可变合同使用。 + * 缓存只证明这次加载结构门禁曾通过,不证明任意后来的 state/权限变化也可沿用结果。 */ export function validateStructuralIntent(intent:OperationIntent,document:Document,registry:Registry,state:LoadingState,source:Capture,assets?:ReadonlyMap):Document { let registries=intentProofs.get(source);if(!registries){registries=new WeakMap();intentProofs.set(source,registries);}let intents=registries.get(registry);if(!intents){intents=new WeakMap();registries.set(registry,intents);}const previous=intents.get(intent);if(previous?.before===document&&previous.assets===assets)return previous.after; const after=assertStructuralChanges(intent.changes,document,registry,state,source,assets);intents.set(intent,{before:document,after,assets});return after;