[docs]classEvent(Record):operation:OpEnumitem_id:UUID|None=None# entity.uidfield:str|None=None# entity.localskey:str|int|UUID|None=None# entity.locals[foo] or registry.get(key)value:Any=None# entity.locals[foo] = bar or registry.add(value)prior_value:Any=Nonedefapply(self,registry:Registry)->None:ifnotisinstance(registry,Registry):raiseValueError(f"Invalid registry type {type(registry)} for patch")item_id=self.item_id# MVP replay uses entity-level CRUD deltas.ifself.operation==OpEnum.CREATE:value=self.valueifisinstance(value,dict):value=Entity.structure(value)ifnotisinstance(value,Entity):raiseValueError("CREATE event requires entity payload")ifitem_idisnotNoneandvalue.uid!=item_id:raiseValueError("CREATE event item_id does not match payload uid")registry.add(value)returnifself.operation==OpEnum.DELETEanditem_idisnotNoneandself.fieldisNone:registry.remove(item_id)returnifself.operation==OpEnum.UPDATEanditem_idisnotNoneandself.fieldisNone:value=self.valueifisinstance(value,dict):value=Entity.structure(value)ifnotisinstance(value,Entity):raiseValueError("UPDATE event requires entity payload")ifvalue.uid!=item_id:raiseValueError("UPDATE event item_id does not match payload uid")# overwrite by uid while preserving key orderregistry.add(value)return# Backward-compatible narrow field update path.ifitem_idisNone:raiseValueError(f"Invalid event {self!r}: missing item_id")item=registry.get(item_id)ifitemisNone:raiseValueError(f"Invalid event {self!r}: target item not found")ifself.keyisNoneandself.fieldisnotNone:ifself.operation==OpEnum.UPDATE:setattr(item,self.field,self.value)returnifself.operation==OpEnum.DELETE:delattr(item,self.field)returnraiseValueError(f"Invalid event {self!r} for item {item!r}.")
[docs]classPatch(Record):registry_id:UUIDinitial_registry_value_hash:bytesfinal_registry_value_hash:bytesevents:list[Event]=Field(default_factory=list)# patch event chains can definitely be canonicalized, reduced by removing# updates followed by deletes, and condensed into single multi-field/key# updates for an item, but that is an optimization concern.def_validate_registry_pre(self,registry:Registry)->bool:ifself.registry_id!=registry.uid:raiseValueError("Invalid registry for patch")ifself.initial_registry_value_hash!=registry.value_hash():raiseValueError("Invalid initial registry state for patch")returnTruedef_validate_registry_post(self,registry:Registry)->bool:ifself.final_registry_value_hash!=registry.value_hash():raiseValueError("Patch failed! Invalid final registry state for patch")returnTruedefapply(self,registry:Registry)->Registry:self._validate_registry_pre(registry)foreventinself.events:event.apply(registry)self._validate_registry_post(registry)returnregistrydefapply_to(self,graph:Graph)->Graph:"""Protocol hook used by replay engines."""self.apply(graph)returngraph