Persist compact history transition evidence with recovery and resource pins

This commit is contained in:
2026-11-08 03:20:50 +08:00
parent 343bfdecff
commit 231f70d87d
9 changed files with 167 additions and 13 deletions
+6 -3
View File
@@ -1,4 +1,5 @@
import type { Change, Document, FieldChange, HistoryState, Id, Json, OperationIntent, Registry } from '../canvas-core/types.ts';
import { describeHistoryTransition, type HistoryTransition } from '../canvas-core/history-transition.ts';
import { invert } from '../canvas-core/document.ts';
import { planHistoryChanges } from '../canvas-core/history.ts';
import { equal, fault, freeze, readMap } from '../canvas-core/values.ts';
@@ -11,7 +12,7 @@ import { referencedResources } from './resources.ts';
export type RecoveryChoice = 'keep-local' | 'discard-local';
export type RecoveryDecision = { readonly operationId: Id; readonly choice: RecoveryChoice; readonly replacementId?: Id; readonly resolution: 'superseded' | 'discarded' | 'satisfied' };
export type RecoveryAudit = { readonly id: Id; readonly formatVersion: 1; readonly baseRootRevision: number; readonly decisions: readonly RecoveryDecision[]; readonly backup: Json };
export type RecoveryAudit = { readonly id: Id; readonly formatVersion: 1; readonly baseRootRevision: number; readonly decisions: readonly RecoveryDecision[]; readonly backup: Json; readonly historyTransition?:HistoryTransition };
export type ConflictPlan = { readonly id: Id; readonly baseRootRevision: number; readonly document: Document; readonly history: HistoryState; readonly operations: EngineSnapshot['operations']; readonly ledger: EngineSnapshot['runtime']['ledger']; readonly audit: RecoveryAudit; readonly resourceIds: readonly Id[]; readonly operationIds: readonly Id[] };
const fieldKey = (change: FieldChange) => JSON.stringify([change.target.id,change.target.incarnation,change.field]);
function value(document: Document, change: FieldChange, allowMissing=false): {before:Json;beforePresent?:boolean;missing?:boolean} {
@@ -59,7 +60,8 @@ export function planConflictRecovery(snapshot: EngineSnapshot, canonical: Canoni
const dependencySet=new Set(dependencyIds);
for(const previous of replacements.values())if(previous)dependencySet.add(previous.id);
const replacementId=unique();
const basic={...intent,id:replacementId,sequence:++sequence,changes:freeze(changes),dependencies:[...dependencySet],canvasIds:operationCanvases({...intent,canvasIds:[],changes},before,document)};
const {historyTransition:_oldHistory,...originalFact}=intent;
const basic={...originalFact,id:replacementId,sequence:++sequence,changes:freeze(changes),dependencies:[...dependencySet],canvasIds:operationCanvases({...intent,canvasIds:[],changes},before,document)};
replacement=intent.operationType==='run.apply'?freeze({...basic,operationType:'run.apply',action:{...intent.action,targetCanvasId:document.elements.get(intent.action.target.id)!.ownerCanvasId}}) as OperationIntent:freeze(basic);
operations.set(replacement.id,replacement);
}
@@ -78,7 +80,8 @@ export function planConflictRecovery(snapshot: EngineSnapshot, canonical: Canoni
for(const record of records)for(const resource of referencedResources(record.intent.changes,registry))resourceIds.add(resource);
for(const replacement of replacements.values())if(replacement)for(const resource of referencedResources(replacement.changes,registry))resourceIds.add(resource);
const backup=JSON.parse(JSON.stringify({document:documentValue(snapshot.document),acknowledged:documentValue(snapshot.runtime.acknowledged),versions:[...snapshot.runtime.versions],resourceIds:[...resourceIds],history:{undo:snapshot.history.undo,redo:snapshot.history.redo,entries:[...snapshot.history.entries]}})) as Json;
const audit=freeze({id,formatVersion:1 as const,baseRootRevision:snapshot.rootRevision,decisions,backup});
const historyTransition=describeHistoryTransition(snapshot.history,history,snapshot.rootRevision+1);
const audit=freeze({id,formatVersion:1 as const,baseRootRevision:snapshot.rootRevision,decisions,backup,...(historyTransition?{historyTransition}:{})});
return Object.freeze({id,baseRootRevision:snapshot.rootRevision,document,history,operations:readMap(operations),ledger:ledger.getSnapshot(),audit,resourceIds:Object.freeze([...resourceIds]),operationIds:Object.freeze([...replacements.values()].flatMap(intent=>intent?[intent.id]:[]))});
}