diff --git a/src/lib/canvas-runtime/history-event-source.ts b/src/lib/canvas-runtime/history-event-source.ts index fd3fd5e..4bcb120 100644 --- a/src/lib/canvas-runtime/history-event-source.ts +++ b/src/lib/canvas-runtime/history-event-source.ts @@ -15,17 +15,24 @@ export type HistorySourceEvent={readonly record:OperationRecord|undefined;readon /** 事件序列与可选 owning adoption 证据;count 标识虚拟事件前缀长度。 */ export type HistoryEventSource={readonly events:readonly HistorySourceEvent[];readonly adoption?:{readonly count:number;readonly replay:HistoryReplay;readonly transition:HistoryTransition}}; /** Resolve either a native interval or the owning adoption's proved virtual events, followed by later native events. */ -/** 找最早有文档效果的 pending 所属事件源;缺失/歧义/陈旧审计不能猜,直接拒绝。 */ +/** 从 pending 中有 changes 的最早 sequence 选择事件源;全为空则返回空列表,不读取其它证据。 + * 无 owning adoption 时取原生区间;有时先取其虚拟 replay.events,再接 revision 之后的原生事件。 + * 这里只解析形状/解释器/先后关系,不反向执行历史或核验事件文档效果;resolution 由消费规划器继续检查。 + * pending 应来自同一已验证账本;ID 顺序检查不等于逐字段比对传入 intent 与 ledger 对象。 + * 返回数组/envelope 未冻结,record/transition 沿用快照引用,调用方按不可变证据使用。 */ export function historyEventSource(snapshot:EngineSnapshot,pending:readonly OperationIntent[]):HistoryEventSource{ const content=pending.filter(intent=>intent.changes.length).sort((a,b)=>a.sequence-b.sequence),first=content[0];if(!first)return{events:[]}; const records=snapshot.runtime.ledger.operations,audits=[...snapshot.runtime.recoveries.values()],owners=audits.filter(audit=>audit.decisions.some(decision=>decision.replacementId===first.id)); if(owners.length>1)throw fault('recovery_history_adoption_ambiguous',[first.id]); - /** 确认操作/尺寸效果解释器存在,未来数据保留不等于允许当前代码解释执行。 */ + /** 要求 operationSchemaVersion=1,存在尺寸效果时验其解释器形状;不在此执行尺寸计划或校验操作类别。 + * 未来数据可被上层保留,不表示此路径能据它恢复历史。 */ const interpreter=(record:OperationRecord)=>{if(record.intent.operationSchemaVersion!==1)throw fault('operation_interpreter_unavailable',[record.intent.id]);if(record.intent.historyEffect)validateCreationSizeEffect(record.intent.historyEffect);}; const pendingIds=new Set(content.map(intent=>intent.id)); - /** 校验事件不重复,且 pending 子序列与逻辑 sequence 精确一致,不能遗漏或交换。 */ + /** 带 record 的所有事件 ID 不重复,抽出的 pending 子序列须与按 sequence 排序的 content 完全对应。 + * adjustment 无 operation ID 不参与此计数;不要求所有账本 sequence 数值连续,也不检查服务执行顺序。 */ const order=(events:readonly HistorySourceEvent[])=>{const seen=new Set(),active:string[]=[];for(const event of events)if(event.record){const id=event.record.intent.id;if(seen.has(id))throw fault('recovery_history_event_order',[id]);seen.add(id);if(pendingIds.has(id))active.push(id);}if(active.length!==content.length||active.some((id,index)=>id!==content[index].id))throw fault('recovery_history_event_order');}; - /** 从操作提取已验证的原生历史 transition,缺证据不构造推测事件。 */ + /** 提取原生 transition 并验结构;未传领域 checkChanges,也不在这里重放其 before/after 元数据。 + * 缺失证据直接拒绝,完整可回放性由 createHistoryJournalRecovery 的反向条件执行验证。 */ const native=(record:OperationRecord):HistorySourceEvent=>{interpreter(record);const transition=record.intent.historyTransition;if(!transition)throw fault('recovery_history_transition_missing',[record.intent.id]);validateHistoryTransition(transition);return{record,transition};}; // 无 owning adoption 时用原生区间;区间内还有 adoption 就不能越过其边界盲目回放。 if(!owners.length){ @@ -33,7 +40,8 @@ export function historyEventSource(snapshot:EngineSnapshot,pending:readonly Oper const start=events[0]?.transition.rootRevision??Infinity;if(audits.some(audit=>(audit.historyTransition?.rootRevision??audit.baseRootRevision+1)>=start))throw fault('recovery_history_adoption_replay_required'); for(let i=1;iother.baseRootRevision+1>revision))throw fault('recovery_history_adoption_stale',[audit.id]); @@ -42,7 +50,8 @@ export function historyEventSource(snapshot:EngineSnapshot,pending:readonly Oper if(event.kind==='adjustment')return{record:undefined,transition:event.transition};const record=records.get(event.operationId);if(!record?.intent.changes.length)throw fault('history_replay_operation_missing',[event.operationId]);interpreter(record);witnessed.add(event.operationId);return{record,transition:event.transition}; }); for(const intent of content)if(!witnessed.has(intent.id)&&!(intent.historyTransition&&intent.historyTransition.rootRevision>revision))throw fault('recovery_history_transition_missing',[intent.id]); - // 只接 adoption revision 之后的原生事件,再核对 revision 单调与 pending 完整顺序。 + // 后续原生事件按逻辑 sequence 排序,再要求 rootRevision 严格递增,不以网络回执顺序拼接。 + // 本层不按 phase/resolution 过滤;消费端仍须拒绝非 pending 但结果未明确的历史贡献。 const later=[...records.values()].filter(record=>record.intent.changes.length&&record.intent.historyTransition&&record.intent.historyTransition.rootRevision>revision).sort((a,b)=>a.intent.sequence-b.intent.sequence).map(native); let previous=revision;for(const event of later){if(event.transition.rootRevision<=previous)throw fault('recovery_history_event_order');previous=event.transition.rootRevision;} order([...events,...later]);return{events:[...events,...later],adoption:{count:events.length,replay,transition:audit.historyTransition}}; diff --git a/src/lib/canvas-runtime/traversal-recovery.ts b/src/lib/canvas-runtime/traversal-recovery.ts index 70e9925..74a7534 100644 --- a/src/lib/canvas-runtime/traversal-recovery.ts +++ b/src/lib/canvas-runtime/traversal-recovery.ts @@ -9,7 +9,10 @@ import { planHistoryChanges } from '../canvas-core/history.ts'; import { fault, freeze, readMap } from '../canvas-core/values.ts'; /** Rewind only proved local stack moves, then replay the explicitly retained traversals. Never publish the temporary cursor. */ -/** 输入原 pending、已审阅替代意图、保留选择与文档边界,返回新双栈/entry 候选而不发布。 */ +/** 兼容的纯遍历恢复:pending 的非空 changes 必须全是 undo/redo,同一历史项最多遍历一次。 + * 输入须为原逻辑顺序且其移动已反映在 snapshot.history;本函数不排序,也不验证恢复选择/替代意图来源。 + * owner 索引由 entry.operationIds 建立,重复关联会覆盖;协调器须先拒绝歧义并提供按 entry ID 的文档边界。 + * 返回私有两栈/entry 候选,不发布文档、发送逆操作或写审计;混合编辑/重复遍历交有完整证据的日志恢复处理。 */ export function replanTraversals(snapshot:EngineSnapshot,pending:readonly OperationIntent[],replacements:ReadonlyMap,kept:ReadonlySet,boundaries:ReadonlyMap,registry:Registry):HistoryState { const traversals=pending.filter(intent=>intent.changes.length); if(traversals.some(intent=>!['undo','redo'].includes(intent.origin)))throw fault('recovery_history_mixed_traversal_not_supported'); @@ -25,12 +28,14 @@ export function replanTraversals(snapshot:EngineSnapshot,pending:readonly Operat const id=entryId(intent);if(seen.has(id))throw fault('recovery_history_repeated_traversal_not_supported',[id]);seen.add(id); if(intent.origin==='undo')move(redo,undo,id);else move(undo,redo,id); } - // 再按原逻辑顺序重放保留选择;每条都核对新的栈顶和正反向 changes 可执行性。 + // 按原顺序逐项审阅,连丢弃项也要求命中当前栈顶;丢弃不重做旧移动,导致后项不可达时明确拒绝。 + // planHistoryChanges 只验证/规划候选方向,结果不在此作为业务文档发布。 for(const intent of traversals){ const id=entryId(intent),entry=entries.get(id)!,source=intent.origin==='undo'?undo:redo,destination=intent.origin==='undo'?redo:undo; if(source.at(-1)!==id)throw fault('recovery_history_traversal_order',[id]); const replacement=replacements.get(intent.id),boundary=boundaries.get(id)!; - // 保留但新基线已满足、没有 replacement 时,退休该项而非制造一条无法重做的假历史。 + // keep 但无 replacement 按协调器给出的“已满足”决定退休该项,不在此重新证明 fresh 文档已满足旧意图。 + // 有 replacement 时将 changes 统一回 entry 的正向语义,并用 boundary.after 检验反向可行性。 if(kept.has(intent.id)){ if(!replacement){source.pop();entries.delete(id);continue;} // No remaining local effect, retired with an audit rather than a fabricated redo. const changes=intent.origin==='undo'?invert(replacement.changes):replacement.changes; @@ -38,7 +43,8 @@ export function replanTraversals(snapshot:EngineSnapshot,pending:readonly Operat catch{throw fault('recovery_history_traversal_conflict',[id]);} move(source,destination,id); entries.set(id,freeze({...entry,changes,operationIds:entry.operationIds.map(operationId=>operationId===intent.id?replacement.id:operationId)})); - // 丢弃该 pending 遍历不等于丢弃原 entry:保留经过校验的正向 changes,并移除旧操作关联。 + // 丢弃 pending 遍历保留原 entry 的正向变化,在 boundary.before 检验原遍历仍可执行,不移动两栈。 + // 只移除此 intent 的关联,保留同 entry 其它贡献/元数据;不将丢弃等同于清空整项历史。 }else{ const changes=intent.origin==='undo'?invert(intent.changes):intent.changes; try{planHistoryChanges(boundary.before,intent.origin==='undo'?invert(changes):changes,registry);} @@ -46,5 +52,6 @@ export function replanTraversals(snapshot:EngineSnapshot,pending:readonly Operat entries.set(id,freeze({...entry,changes,operationIds:entry.operationIds.filter(operationId=>operationId!==intent.id)})); } } + // 未改 entry 沿用不可变记录引用;冻结两栈并捕获表成员,不替代外围对恢复 Root 的整体校验。 return Object.freeze({undo:freeze(undo),redo:freeze(redo),entries:readMap(entries)}); }