git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2883 lines
95 KiB
Markdown
2883 lines
95 KiB
Markdown
# ADR-016: Delta-Behavior System - Domain-Driven Design Architecture
|
|
|
|
**Status**: Proposed
|
|
**Date**: 2026-01-28
|
|
**Parent**: ADR-001 RuVector Core Architecture
|
|
**Author**: System Architecture Designer
|
|
|
|
## Abstract
|
|
|
|
This ADR defines a comprehensive Domain-Driven Design (DDD) architecture for a "Delta-Behavior" system using RuVector WASM modules. The system captures, propagates, aggregates, and applies differential changes (deltas) to vector representations, enabling efficient incremental updates, temporal versioning, and distributed state synchronization.
|
|
|
|
---
|
|
|
|
## 1. Executive Summary
|
|
|
|
The Delta-Behavior system models state changes as first-class domain objects rather than simple mutations. By treating deltas as immutable, causally-ordered events, the system enables:
|
|
|
|
- **Efficient incremental updates**: Only transmit/store changes, not full states
|
|
- **Temporal queries**: Reconstruct any historical state via delta replay
|
|
- **Conflict detection**: Identify and resolve concurrent modifications
|
|
- **Distributed sync**: Propagate deltas across nodes with eventual consistency
|
|
- **WASM portability**: Core logic runs in browser, edge, and server environments
|
|
|
|
---
|
|
|
|
## 2. Domain Analysis
|
|
|
|
### 2.1 Strategic Domain Design
|
|
|
|
The Delta-Behavior system spans five bounded contexts, each representing a distinct subdomain:
|
|
|
|
```
|
|
+------------------------------------------------------------------+
|
|
| DELTA-BEHAVIOR SYSTEM |
|
|
+------------------------------------------------------------------+
|
|
| |
|
|
| +----------------+ +-------------------+ +----------------+ |
|
|
| | Delta Capture | | Delta Propagation | | Delta | |
|
|
| | Domain |--->| Domain |--->| Aggregation | |
|
|
| | | | | | Domain | |
|
|
| | - Observers | | - Routers | | | |
|
|
| | - Detectors | | - Channels | | - Windows | |
|
|
| | - Extractors | | - Subscribers | | - Batchers | |
|
|
| +----------------+ +-------------------+ +-------+--------+ |
|
|
| | |
|
|
| v |
|
|
| +----------------+ +-------------------+ +----------------+ |
|
|
| | Delta |<---| Delta Application |<---| (Aggregated | |
|
|
| | Versioning | | Domain | | Deltas) | |
|
|
| | Domain | | | +----------------+ |
|
|
| | | | - Applicators | |
|
|
| | - History | | - Validators | |
|
|
| | - Snapshots | | - Transformers | |
|
|
| | - Branches | | | |
|
|
| +----------------+ +-------------------+ |
|
|
| |
|
|
+------------------------------------------------------------------+
|
|
```
|
|
|
|
### 2.2 Core Domain Concepts
|
|
|
|
| Domain Concept | Definition |
|
|
|----------------|------------|
|
|
| **Delta** | An immutable record of a differential change between two states |
|
|
| **DeltaStream** | Ordered sequence of deltas forming a causal chain |
|
|
| **DeltaGraph** | DAG structure representing delta dependencies and branches |
|
|
| **DeltaWindow** | Temporal container for batching deltas within a time/count boundary |
|
|
| **DeltaVector** | Sparse representation of the actual change data (vector diff) |
|
|
| **DeltaCheckpoint** | Full state snapshot at a specific delta sequence point |
|
|
|
|
---
|
|
|
|
## 3. Bounded Context Definitions
|
|
|
|
### 3.1 Delta Capture Domain
|
|
|
|
**Purpose**: Detect state changes and extract delta representations from source systems.
|
|
|
|
#### Ubiquitous Language
|
|
|
|
| Term | Definition |
|
|
|------|------------|
|
|
| **Observer** | Component that monitors a source for state changes |
|
|
| **ChangeEvent** | Raw notification that a state modification occurred |
|
|
| **Detector** | Algorithm that identifies meaningful changes vs noise |
|
|
| **Extractor** | Component that computes the delta between old and new state |
|
|
| **CapturePolicy** | Rules governing when/how deltas are captured |
|
|
| **SourceBinding** | Connection between observer and monitored resource |
|
|
|
|
#### Aggregate Roots
|
|
|
|
```rust
|
|
/// Delta Capture Domain - Aggregate Roots and Entities
|
|
pub mod delta_capture {
|
|
use std::collections::HashMap;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
// ============================================================
|
|
// VALUE OBJECTS
|
|
// ============================================================
|
|
|
|
/// Unique identifier for a delta
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
|
|
pub struct DeltaId(pub u128);
|
|
|
|
impl DeltaId {
|
|
pub fn new() -> Self {
|
|
Self(uuid::Uuid::new_v4().as_u128())
|
|
}
|
|
|
|
pub fn from_bytes(bytes: [u8; 16]) -> Self {
|
|
Self(u128::from_be_bytes(bytes))
|
|
}
|
|
|
|
pub fn to_bytes(&self) -> [u8; 16] {
|
|
self.0.to_be_bytes()
|
|
}
|
|
}
|
|
|
|
/// Logical timestamp for causal ordering
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
|
|
pub struct DeltaTimestamp {
|
|
/// Logical clock (Lamport timestamp)
|
|
pub logical: u64,
|
|
/// Physical wall-clock time (milliseconds since epoch)
|
|
pub physical: u64,
|
|
/// Node identifier for tie-breaking
|
|
pub node_id: u32,
|
|
}
|
|
|
|
impl DeltaTimestamp {
|
|
pub fn new(logical: u64, physical: u64, node_id: u32) -> Self {
|
|
Self { logical, physical, node_id }
|
|
}
|
|
|
|
/// Advance logical clock, ensuring it's ahead of physical time
|
|
pub fn tick(&self) -> Self {
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_millis() as u64;
|
|
|
|
Self {
|
|
logical: self.logical + 1,
|
|
physical: now_ms.max(self.physical),
|
|
node_id: self.node_id,
|
|
}
|
|
}
|
|
|
|
/// Merge with another timestamp (for receiving events)
|
|
pub fn merge(&self, other: &DeltaTimestamp) -> Self {
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_millis() as u64;
|
|
|
|
Self {
|
|
logical: self.logical.max(other.logical) + 1,
|
|
physical: now_ms.max(self.physical).max(other.physical),
|
|
node_id: self.node_id,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Checksum for delta integrity verification
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
|
pub struct DeltaChecksum(pub [u8; 32]);
|
|
|
|
impl DeltaChecksum {
|
|
/// Compute Blake3 hash of delta payload
|
|
pub fn compute(data: &[u8]) -> Self {
|
|
use sha2::{Sha256, Digest};
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(data);
|
|
let result = hasher.finalize();
|
|
let mut bytes = [0u8; 32];
|
|
bytes.copy_from_slice(&result);
|
|
Self(bytes)
|
|
}
|
|
|
|
/// Chain with previous checksum for tamper-evidence
|
|
pub fn chain(&self, previous: &DeltaChecksum, data: &[u8]) -> Self {
|
|
use sha2::{Sha256, Digest};
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(&previous.0);
|
|
hasher.update(data);
|
|
let result = hasher.finalize();
|
|
let mut bytes = [0u8; 32];
|
|
bytes.copy_from_slice(&result);
|
|
Self(bytes)
|
|
}
|
|
}
|
|
|
|
/// Magnitude/size metric for a delta
|
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
|
pub struct DeltaMagnitude {
|
|
/// Number of dimensions changed
|
|
pub dimensions_changed: u32,
|
|
/// Total L2 norm of the change
|
|
pub l2_norm: f32,
|
|
/// Maximum single-dimension change
|
|
pub max_component: f32,
|
|
/// Sparsity ratio (changed/total dimensions)
|
|
pub sparsity: f32,
|
|
}
|
|
|
|
impl DeltaMagnitude {
|
|
pub fn compute(old: &[f32], new: &[f32]) -> Self {
|
|
assert_eq!(old.len(), new.len());
|
|
|
|
let mut dims_changed = 0u32;
|
|
let mut l2_sum = 0.0f32;
|
|
let mut max_comp = 0.0f32;
|
|
|
|
for (o, n) in old.iter().zip(new.iter()) {
|
|
let diff = (n - o).abs();
|
|
if diff > f32::EPSILON {
|
|
dims_changed += 1;
|
|
l2_sum += diff * diff;
|
|
max_comp = max_comp.max(diff);
|
|
}
|
|
}
|
|
|
|
Self {
|
|
dimensions_changed: dims_changed,
|
|
l2_norm: l2_sum.sqrt(),
|
|
max_component: max_comp,
|
|
sparsity: dims_changed as f32 / old.len() as f32,
|
|
}
|
|
}
|
|
|
|
/// Check if delta is significant enough to record
|
|
pub fn is_significant(&self, threshold: f32) -> bool {
|
|
self.l2_norm > threshold
|
|
}
|
|
}
|
|
|
|
/// Sparse representation of vector changes
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct DeltaVector {
|
|
/// Total dimensions of the full vector
|
|
pub total_dims: u32,
|
|
/// Indices of changed dimensions
|
|
pub indices: Vec<u32>,
|
|
/// Delta values (new - old) for each changed index
|
|
pub values: Vec<f32>,
|
|
/// Magnitude metrics
|
|
pub magnitude: DeltaMagnitude,
|
|
}
|
|
|
|
impl DeltaVector {
|
|
/// Create from old and new vectors
|
|
pub fn from_diff(old: &[f32], new: &[f32], min_diff: f32) -> Self {
|
|
assert_eq!(old.len(), new.len());
|
|
|
|
let mut indices = Vec::new();
|
|
let mut values = Vec::new();
|
|
|
|
for (i, (o, n)) in old.iter().zip(new.iter()).enumerate() {
|
|
let diff = n - o;
|
|
if diff.abs() > min_diff {
|
|
indices.push(i as u32);
|
|
values.push(diff);
|
|
}
|
|
}
|
|
|
|
Self {
|
|
total_dims: old.len() as u32,
|
|
indices,
|
|
values,
|
|
magnitude: DeltaMagnitude::compute(old, new),
|
|
}
|
|
}
|
|
|
|
/// Apply delta to a base vector
|
|
pub fn apply(&self, base: &mut [f32]) {
|
|
assert_eq!(base.len(), self.total_dims as usize);
|
|
|
|
for (&idx, &val) in self.indices.iter().zip(self.values.iter()) {
|
|
base[idx as usize] += val;
|
|
}
|
|
}
|
|
|
|
/// Invert delta (for rollback)
|
|
pub fn invert(&self) -> Self {
|
|
Self {
|
|
total_dims: self.total_dims,
|
|
indices: self.indices.clone(),
|
|
values: self.values.iter().map(|v| -v).collect(),
|
|
magnitude: self.magnitude,
|
|
}
|
|
}
|
|
|
|
/// Compose two deltas (this then other)
|
|
pub fn compose(&self, other: &DeltaVector) -> Self {
|
|
assert_eq!(self.total_dims, other.total_dims);
|
|
|
|
let mut combined: HashMap<u32, f32> = HashMap::new();
|
|
|
|
for (&idx, &val) in self.indices.iter().zip(self.values.iter()) {
|
|
*combined.entry(idx).or_insert(0.0) += val;
|
|
}
|
|
for (&idx, &val) in other.indices.iter().zip(other.values.iter()) {
|
|
*combined.entry(idx).or_insert(0.0) += val;
|
|
}
|
|
|
|
// Filter out zero changes
|
|
let filtered: Vec<_> = combined.into_iter()
|
|
.filter(|(_, v)| v.abs() > f32::EPSILON)
|
|
.collect();
|
|
|
|
let mut indices: Vec<u32> = filtered.iter().map(|(i, _)| *i).collect();
|
|
indices.sort();
|
|
|
|
let values: Vec<f32> = indices.iter()
|
|
.map(|i| filtered.iter().find(|(idx, _)| idx == i).unwrap().1)
|
|
.collect();
|
|
|
|
Self {
|
|
total_dims: self.total_dims,
|
|
indices,
|
|
values,
|
|
magnitude: DeltaMagnitude {
|
|
dimensions_changed: filtered.len() as u32,
|
|
l2_norm: values.iter().map(|v| v * v).sum::<f32>().sqrt(),
|
|
max_component: values.iter().map(|v| v.abs()).fold(0.0, f32::max),
|
|
sparsity: filtered.len() as f32 / self.total_dims as f32,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Serialize to bytes
|
|
pub fn to_bytes(&self) -> Vec<u8> {
|
|
bincode::serialize(self).unwrap()
|
|
}
|
|
|
|
/// Deserialize from bytes
|
|
pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
|
|
bincode::deserialize(bytes)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// AGGREGATES
|
|
// ============================================================
|
|
|
|
/// Source identifier being observed
|
|
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
|
|
pub struct SourceId(pub String);
|
|
|
|
/// Observer configuration and state
|
|
#[derive(Clone, Debug)]
|
|
pub struct Observer {
|
|
pub id: ObserverId,
|
|
pub source_id: SourceId,
|
|
pub capture_policy: CapturePolicy,
|
|
pub status: ObserverStatus,
|
|
pub last_capture: Option<DeltaTimestamp>,
|
|
pub metrics: ObserverMetrics,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
pub struct ObserverId(pub u64);
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum ObserverStatus {
|
|
Active,
|
|
Paused,
|
|
Error,
|
|
Terminated,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct CapturePolicy {
|
|
/// Minimum time between captures (milliseconds)
|
|
pub min_interval_ms: u64,
|
|
/// Minimum magnitude threshold for capture
|
|
pub magnitude_threshold: f32,
|
|
/// Maximum deltas to buffer before force-flush
|
|
pub buffer_limit: usize,
|
|
/// Whether to capture zero-deltas as heartbeats
|
|
pub capture_heartbeats: bool,
|
|
}
|
|
|
|
impl Default for CapturePolicy {
|
|
fn default() -> Self {
|
|
Self {
|
|
min_interval_ms: 100,
|
|
magnitude_threshold: 1e-6,
|
|
buffer_limit: 1000,
|
|
capture_heartbeats: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct ObserverMetrics {
|
|
pub deltas_captured: u64,
|
|
pub deltas_filtered: u64,
|
|
pub bytes_processed: u64,
|
|
pub avg_magnitude: f32,
|
|
pub last_error: Option<String>,
|
|
}
|
|
|
|
// ============================================================
|
|
// DELTA AGGREGATE ROOT
|
|
// ============================================================
|
|
|
|
/// The core Delta entity - immutable once created
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct Delta {
|
|
/// Unique identifier
|
|
pub id: DeltaId,
|
|
/// Source that produced this delta
|
|
pub source_id: SourceId,
|
|
/// Causal timestamp
|
|
pub timestamp: DeltaTimestamp,
|
|
/// Previous delta in the chain (None for genesis)
|
|
pub parent_id: Option<DeltaId>,
|
|
/// The actual change data
|
|
pub vector: DeltaVector,
|
|
/// Integrity checksum (chained with parent)
|
|
pub checksum: DeltaChecksum,
|
|
/// Additional metadata
|
|
pub metadata: HashMap<String, String>,
|
|
}
|
|
|
|
impl Delta {
|
|
/// Create a new delta
|
|
pub fn new(
|
|
source_id: SourceId,
|
|
timestamp: DeltaTimestamp,
|
|
parent: Option<&Delta>,
|
|
vector: DeltaVector,
|
|
metadata: HashMap<String, String>,
|
|
) -> Self {
|
|
let id = DeltaId::new();
|
|
let parent_id = parent.map(|p| p.id);
|
|
|
|
let payload = vector.to_bytes();
|
|
let checksum = match parent {
|
|
Some(p) => p.checksum.chain(&p.checksum, &payload),
|
|
None => DeltaChecksum::compute(&payload),
|
|
};
|
|
|
|
Self {
|
|
id,
|
|
source_id,
|
|
timestamp,
|
|
parent_id,
|
|
vector,
|
|
checksum,
|
|
metadata,
|
|
}
|
|
}
|
|
|
|
/// Verify checksum chain integrity
|
|
pub fn verify_chain(&self, parent: Option<&Delta>) -> bool {
|
|
let payload = self.vector.to_bytes();
|
|
let expected = match parent {
|
|
Some(p) => p.checksum.chain(&p.checksum, &payload),
|
|
None => DeltaChecksum::compute(&payload),
|
|
};
|
|
self.checksum == expected
|
|
}
|
|
|
|
/// Check if this delta is a descendant of another
|
|
pub fn is_descendant_of(&self, ancestor_id: DeltaId) -> bool {
|
|
self.parent_id == Some(ancestor_id)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Domain Events
|
|
|
|
| Event | Payload | Published When |
|
|
|-------|---------|----------------|
|
|
| `ChangeDetected` | source_id, old_state_hash, new_state_hash | Observer detects state modification |
|
|
| `DeltaExtracted` | delta_id, source_id, magnitude | Delta computed from change |
|
|
| `DeltaCaptured` | delta_id, timestamp, checksum | Delta committed to capture buffer |
|
|
| `CaptureBufferFlushed` | delta_count, batch_id | Buffer contents sent downstream |
|
|
| `ObserverError` | observer_id, error_type, message | Capture failure |
|
|
| `ObserverPaused` | observer_id, reason | Observer temporarily stopped |
|
|
|
|
#### Domain Services
|
|
|
|
```rust
|
|
/// Domain services for Delta Capture
|
|
pub mod capture_services {
|
|
use super::delta_capture::*;
|
|
|
|
/// Trait for change detection algorithms
|
|
pub trait ChangeDetector: Send + Sync {
|
|
/// Compare states and determine if change is significant
|
|
fn detect(&self, old: &[f32], new: &[f32], policy: &CapturePolicy) -> bool;
|
|
|
|
/// Get detector configuration
|
|
fn config(&self) -> DetectorConfig;
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct DetectorConfig {
|
|
pub algorithm: String,
|
|
pub threshold: f32,
|
|
pub use_cosine: bool,
|
|
}
|
|
|
|
/// Default detector using L2 norm threshold
|
|
pub struct L2ThresholdDetector {
|
|
pub threshold: f32,
|
|
}
|
|
|
|
impl ChangeDetector for L2ThresholdDetector {
|
|
fn detect(&self, old: &[f32], new: &[f32], policy: &CapturePolicy) -> bool {
|
|
let magnitude = DeltaMagnitude::compute(old, new);
|
|
magnitude.l2_norm > self.threshold.max(policy.magnitude_threshold)
|
|
}
|
|
|
|
fn config(&self) -> DetectorConfig {
|
|
DetectorConfig {
|
|
algorithm: "l2_threshold".to_string(),
|
|
threshold: self.threshold,
|
|
use_cosine: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Trait for delta extraction
|
|
pub trait DeltaExtractor: Send + Sync {
|
|
/// Extract delta from state transition
|
|
fn extract(
|
|
&self,
|
|
source_id: &SourceId,
|
|
old_state: &[f32],
|
|
new_state: &[f32],
|
|
timestamp: DeltaTimestamp,
|
|
parent: Option<&Delta>,
|
|
) -> Delta;
|
|
}
|
|
|
|
/// Default sparse delta extractor
|
|
pub struct SparseDeltaExtractor {
|
|
pub min_component_diff: f32,
|
|
}
|
|
|
|
impl DeltaExtractor for SparseDeltaExtractor {
|
|
fn extract(
|
|
&self,
|
|
source_id: &SourceId,
|
|
old_state: &[f32],
|
|
new_state: &[f32],
|
|
timestamp: DeltaTimestamp,
|
|
parent: Option<&Delta>,
|
|
) -> Delta {
|
|
let vector = DeltaVector::from_diff(old_state, new_state, self.min_component_diff);
|
|
Delta::new(
|
|
source_id.clone(),
|
|
timestamp,
|
|
parent,
|
|
vector,
|
|
std::collections::HashMap::new(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Capture orchestration service
|
|
pub trait CaptureService: Send + Sync {
|
|
/// Register an observer for a source
|
|
fn register_observer(
|
|
&mut self,
|
|
source_id: SourceId,
|
|
policy: CapturePolicy,
|
|
) -> Result<ObserverId, CaptureError>;
|
|
|
|
/// Process a state change notification
|
|
fn on_state_change(
|
|
&mut self,
|
|
observer_id: ObserverId,
|
|
old_state: &[f32],
|
|
new_state: &[f32],
|
|
) -> Result<Option<Delta>, CaptureError>;
|
|
|
|
/// Flush buffered deltas
|
|
fn flush(&mut self, observer_id: ObserverId) -> Result<Vec<Delta>, CaptureError>;
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum CaptureError {
|
|
ObserverNotFound(ObserverId),
|
|
PolicyViolation(String),
|
|
ExtractionFailed(String),
|
|
BufferOverflow,
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.2 Delta Propagation Domain
|
|
|
|
**Purpose**: Route deltas through the system to interested subscribers with ordering guarantees.
|
|
|
|
#### Ubiquitous Language
|
|
|
|
| Term | Definition |
|
|
|------|------------|
|
|
| **Channel** | Named conduit for delta transmission |
|
|
| **Subscriber** | Consumer registered to receive deltas from channels |
|
|
| **Router** | Component that directs deltas to appropriate channels |
|
|
| **RoutingPolicy** | Rules for delta channel assignment |
|
|
| **Backpressure** | Flow control mechanism when subscribers are slow |
|
|
| **DeliveryGuarantee** | At-least-once, at-most-once, or exactly-once semantics |
|
|
|
|
#### Aggregate Roots
|
|
|
|
```rust
|
|
/// Delta Propagation Domain
|
|
pub mod delta_propagation {
|
|
use super::delta_capture::*;
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
// ============================================================
|
|
// VALUE OBJECTS
|
|
// ============================================================
|
|
|
|
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
|
pub struct ChannelId(pub String);
|
|
|
|
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
|
pub struct SubscriberId(pub u64);
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum DeliveryGuarantee {
|
|
/// Fire and forget
|
|
AtMostOnce,
|
|
/// Retry until acknowledged
|
|
AtLeastOnce,
|
|
/// Deduplicated delivery
|
|
ExactlyOnce,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum SubscriberStatus {
|
|
Active,
|
|
Paused,
|
|
Backpressured,
|
|
Disconnected,
|
|
}
|
|
|
|
/// Filter for selective subscription
|
|
#[derive(Clone, Debug)]
|
|
pub struct SubscriptionFilter {
|
|
/// Source patterns to match (glob-style)
|
|
pub source_patterns: Vec<String>,
|
|
/// Minimum magnitude to receive
|
|
pub min_magnitude: Option<f32>,
|
|
/// Metadata key-value matches
|
|
pub metadata_filters: HashMap<String, String>,
|
|
}
|
|
|
|
impl SubscriptionFilter {
|
|
pub fn matches(&self, delta: &Delta) -> bool {
|
|
// Check source pattern
|
|
let source_match = self.source_patterns.is_empty() ||
|
|
self.source_patterns.iter().any(|pat| {
|
|
glob_match(pat, &delta.source_id.0)
|
|
});
|
|
|
|
// Check magnitude
|
|
let magnitude_match = self.min_magnitude
|
|
.map(|min| delta.vector.magnitude.l2_norm >= min)
|
|
.unwrap_or(true);
|
|
|
|
// Check metadata
|
|
let metadata_match = self.metadata_filters.iter().all(|(k, v)| {
|
|
delta.metadata.get(k).map(|mv| mv == v).unwrap_or(false)
|
|
});
|
|
|
|
source_match && magnitude_match && metadata_match
|
|
}
|
|
}
|
|
|
|
fn glob_match(pattern: &str, text: &str) -> bool {
|
|
// Simple glob matching (* = any)
|
|
if pattern == "*" { return true; }
|
|
if pattern.contains('*') {
|
|
let parts: Vec<&str> = pattern.split('*').collect();
|
|
if parts.len() == 2 {
|
|
return text.starts_with(parts[0]) && text.ends_with(parts[1]);
|
|
}
|
|
}
|
|
pattern == text
|
|
}
|
|
|
|
// ============================================================
|
|
// AGGREGATES
|
|
// ============================================================
|
|
|
|
/// Channel for delta distribution
|
|
#[derive(Clone, Debug)]
|
|
pub struct Channel {
|
|
pub id: ChannelId,
|
|
pub name: String,
|
|
pub delivery_guarantee: DeliveryGuarantee,
|
|
pub subscribers: HashSet<SubscriberId>,
|
|
pub metrics: ChannelMetrics,
|
|
pub created_at: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct ChannelMetrics {
|
|
pub deltas_published: u64,
|
|
pub deltas_delivered: u64,
|
|
pub avg_latency_ms: f32,
|
|
pub subscriber_count: u32,
|
|
}
|
|
|
|
impl Channel {
|
|
pub fn new(id: ChannelId, name: String, guarantee: DeliveryGuarantee) -> Self {
|
|
Self {
|
|
id,
|
|
name,
|
|
delivery_guarantee: guarantee,
|
|
subscribers: HashSet::new(),
|
|
metrics: ChannelMetrics::default(),
|
|
created_at: std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs(),
|
|
}
|
|
}
|
|
|
|
pub fn add_subscriber(&mut self, sub_id: SubscriberId) -> bool {
|
|
self.subscribers.insert(sub_id)
|
|
}
|
|
|
|
pub fn remove_subscriber(&mut self, sub_id: &SubscriberId) -> bool {
|
|
self.subscribers.remove(sub_id)
|
|
}
|
|
}
|
|
|
|
/// Subscriber registration
|
|
#[derive(Clone, Debug)]
|
|
pub struct Subscriber {
|
|
pub id: SubscriberId,
|
|
pub name: String,
|
|
pub channels: HashSet<ChannelId>,
|
|
pub filter: SubscriptionFilter,
|
|
pub status: SubscriberStatus,
|
|
pub cursor: SubscriberCursor,
|
|
pub metrics: SubscriberMetrics,
|
|
}
|
|
|
|
/// Tracks subscriber progress through delta stream
|
|
#[derive(Clone, Debug)]
|
|
pub struct SubscriberCursor {
|
|
/// Last acknowledged delta per channel
|
|
pub last_acked: HashMap<ChannelId, DeltaId>,
|
|
/// Last timestamp received
|
|
pub last_timestamp: Option<DeltaTimestamp>,
|
|
/// Pending deltas awaiting acknowledgment
|
|
pub pending_count: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct SubscriberMetrics {
|
|
pub deltas_received: u64,
|
|
pub deltas_acked: u64,
|
|
pub avg_processing_time_ms: f32,
|
|
pub backpressure_events: u32,
|
|
}
|
|
|
|
/// Routing decision for a delta
|
|
#[derive(Clone, Debug)]
|
|
pub struct RoutingDecision {
|
|
pub delta_id: DeltaId,
|
|
pub target_channels: Vec<ChannelId>,
|
|
pub priority: RoutingPriority,
|
|
pub ttl_ms: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub enum RoutingPriority {
|
|
Low = 0,
|
|
Normal = 1,
|
|
High = 2,
|
|
Critical = 3,
|
|
}
|
|
|
|
/// Routing policy definition
|
|
#[derive(Clone, Debug)]
|
|
pub struct RoutingPolicy {
|
|
pub id: String,
|
|
pub source_pattern: String,
|
|
pub target_channels: Vec<ChannelId>,
|
|
pub priority: RoutingPriority,
|
|
pub conditions: Vec<RoutingCondition>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum RoutingCondition {
|
|
MinMagnitude(f32),
|
|
MetadataEquals(String, String),
|
|
MetadataExists(String),
|
|
TimeOfDay { start_hour: u8, end_hour: u8 },
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Domain Events
|
|
|
|
| Event | Payload | Published When |
|
|
|-------|---------|----------------|
|
|
| `DeltaRouted` | delta_id, channel_ids, priority | Router assigns delta to channels |
|
|
| `DeltaPublished` | delta_id, channel_id, subscriber_count | Delta sent to channel |
|
|
| `DeltaDelivered` | delta_id, subscriber_id, latency_ms | Subscriber receives delta |
|
|
| `DeltaAcknowledged` | delta_id, subscriber_id | Subscriber confirms processing |
|
|
| `SubscriberBackpressured` | subscriber_id, pending_count | Subscriber overwhelmed |
|
|
| `ChannelCreated` | channel_id, delivery_guarantee | New channel registered |
|
|
| `SubscriptionChanged` | subscriber_id, added_channels, removed_channels | Subscription modified |
|
|
|
|
#### Domain Services
|
|
|
|
```rust
|
|
/// Domain services for Delta Propagation
|
|
pub mod propagation_services {
|
|
use super::delta_propagation::*;
|
|
use super::delta_capture::*;
|
|
|
|
/// Router service for delta distribution
|
|
pub trait DeltaRouter: Send + Sync {
|
|
/// Determine target channels for a delta
|
|
fn route(&self, delta: &Delta) -> RoutingDecision;
|
|
|
|
/// Register a routing policy
|
|
fn add_policy(&mut self, policy: RoutingPolicy) -> Result<(), RouterError>;
|
|
|
|
/// Remove a routing policy
|
|
fn remove_policy(&mut self, policy_id: &str) -> Result<(), RouterError>;
|
|
}
|
|
|
|
/// Channel management service
|
|
pub trait ChannelService: Send + Sync {
|
|
/// Create a new channel
|
|
fn create_channel(
|
|
&mut self,
|
|
id: ChannelId,
|
|
name: String,
|
|
guarantee: DeliveryGuarantee,
|
|
) -> Result<Channel, ChannelError>;
|
|
|
|
/// Publish delta to channel
|
|
fn publish(&mut self, channel_id: &ChannelId, delta: Delta) -> Result<u32, ChannelError>;
|
|
|
|
/// Get channel statistics
|
|
fn get_metrics(&self, channel_id: &ChannelId) -> Option<ChannelMetrics>;
|
|
}
|
|
|
|
/// Subscription management service
|
|
pub trait SubscriptionService: Send + Sync {
|
|
/// Create subscriber
|
|
fn subscribe(
|
|
&mut self,
|
|
name: String,
|
|
channels: Vec<ChannelId>,
|
|
filter: SubscriptionFilter,
|
|
) -> Result<SubscriberId, SubscriptionError>;
|
|
|
|
/// Acknowledge delta receipt
|
|
fn acknowledge(
|
|
&mut self,
|
|
subscriber_id: SubscriberId,
|
|
delta_id: DeltaId,
|
|
) -> Result<(), SubscriptionError>;
|
|
|
|
/// Get pending deltas for subscriber
|
|
fn poll(
|
|
&self,
|
|
subscriber_id: SubscriberId,
|
|
max_count: usize,
|
|
) -> Result<Vec<Delta>, SubscriptionError>;
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum RouterError {
|
|
PolicyConflict(String),
|
|
InvalidPattern(String),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum ChannelError {
|
|
NotFound(ChannelId),
|
|
AlreadyExists(ChannelId),
|
|
PublishFailed(String),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum SubscriptionError {
|
|
SubscriberNotFound(SubscriberId),
|
|
ChannelNotFound(ChannelId),
|
|
Backpressured,
|
|
InvalidFilter(String),
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.3 Delta Aggregation Domain
|
|
|
|
**Purpose**: Combine, batch, and compress deltas for efficient storage and transmission.
|
|
|
|
#### Ubiquitous Language
|
|
|
|
| Term | Definition |
|
|
|------|------------|
|
|
| **DeltaWindow** | Temporal container grouping deltas by time or count |
|
|
| **Batch** | Collection of deltas packaged for bulk processing |
|
|
| **Compaction** | Process of merging sequential deltas into fewer |
|
|
| **Compression** | Reducing delta byte size through encoding |
|
|
| **WindowPolicy** | Rules for window boundaries (time, count, size) |
|
|
| **AggregatedDelta** | Result of combining multiple deltas |
|
|
|
|
#### Aggregate Roots
|
|
|
|
```rust
|
|
/// Delta Aggregation Domain
|
|
pub mod delta_aggregation {
|
|
use super::delta_capture::*;
|
|
use std::collections::HashMap;
|
|
|
|
// ============================================================
|
|
// VALUE OBJECTS
|
|
// ============================================================
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
pub struct WindowId(pub u64);
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
pub struct BatchId(pub u64);
|
|
|
|
/// Window boundary policy
|
|
#[derive(Clone, Debug)]
|
|
pub struct WindowPolicy {
|
|
/// Maximum time span in milliseconds
|
|
pub max_duration_ms: u64,
|
|
/// Maximum delta count
|
|
pub max_count: usize,
|
|
/// Maximum aggregate size in bytes
|
|
pub max_bytes: usize,
|
|
/// Force window close on these events
|
|
pub close_on_metadata: Vec<String>,
|
|
}
|
|
|
|
impl Default for WindowPolicy {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_duration_ms: 1000,
|
|
max_count: 100,
|
|
max_bytes: 1024 * 1024, // 1MB
|
|
close_on_metadata: vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum WindowStatus {
|
|
Open,
|
|
Closing,
|
|
Closed,
|
|
Compacted,
|
|
}
|
|
|
|
// ============================================================
|
|
// AGGREGATES
|
|
// ============================================================
|
|
|
|
/// Temporal window for delta collection
|
|
#[derive(Clone, Debug)]
|
|
pub struct DeltaWindow {
|
|
pub id: WindowId,
|
|
pub source_id: SourceId,
|
|
pub policy: WindowPolicy,
|
|
pub status: WindowStatus,
|
|
/// Deltas in this window (ordered by timestamp)
|
|
pub deltas: Vec<Delta>,
|
|
/// Window start timestamp
|
|
pub started_at: DeltaTimestamp,
|
|
/// Window close timestamp (if closed)
|
|
pub closed_at: Option<DeltaTimestamp>,
|
|
/// Aggregate metrics
|
|
pub metrics: WindowMetrics,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct WindowMetrics {
|
|
pub delta_count: u32,
|
|
pub total_bytes: u64,
|
|
pub total_magnitude: f32,
|
|
pub dimensions_touched: u32,
|
|
}
|
|
|
|
impl DeltaWindow {
|
|
pub fn new(id: WindowId, source_id: SourceId, policy: WindowPolicy, start: DeltaTimestamp) -> Self {
|
|
Self {
|
|
id,
|
|
source_id,
|
|
policy,
|
|
status: WindowStatus::Open,
|
|
deltas: Vec::new(),
|
|
started_at: start,
|
|
closed_at: None,
|
|
metrics: WindowMetrics::default(),
|
|
}
|
|
}
|
|
|
|
/// Check if window should close
|
|
pub fn should_close(&self, current_time_ms: u64) -> bool {
|
|
if self.status != WindowStatus::Open {
|
|
return false;
|
|
}
|
|
|
|
// Time limit
|
|
let elapsed = current_time_ms - self.started_at.physical;
|
|
if elapsed >= self.policy.max_duration_ms {
|
|
return true;
|
|
}
|
|
|
|
// Count limit
|
|
if self.deltas.len() >= self.policy.max_count {
|
|
return true;
|
|
}
|
|
|
|
// Size limit
|
|
if self.metrics.total_bytes as usize >= self.policy.max_bytes {
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Add delta to window
|
|
pub fn add(&mut self, delta: Delta) -> Result<(), WindowError> {
|
|
if self.status != WindowStatus::Open {
|
|
return Err(WindowError::WindowClosed);
|
|
}
|
|
|
|
// Check for close-on-metadata triggers
|
|
for key in &self.policy.close_on_metadata {
|
|
if delta.metadata.contains_key(key) {
|
|
self.status = WindowStatus::Closing;
|
|
}
|
|
}
|
|
|
|
let delta_bytes = delta.vector.to_bytes().len() as u64;
|
|
self.metrics.delta_count += 1;
|
|
self.metrics.total_bytes += delta_bytes;
|
|
self.metrics.total_magnitude += delta.vector.magnitude.l2_norm;
|
|
self.metrics.dimensions_touched = self.metrics.dimensions_touched
|
|
.max(delta.vector.magnitude.dimensions_changed);
|
|
|
|
self.deltas.push(delta);
|
|
Ok(())
|
|
}
|
|
|
|
/// Close the window
|
|
pub fn close(&mut self, timestamp: DeltaTimestamp) {
|
|
self.status = WindowStatus::Closed;
|
|
self.closed_at = Some(timestamp);
|
|
}
|
|
|
|
/// Compact all deltas into an aggregated delta
|
|
pub fn compact(&mut self) -> Option<AggregatedDelta> {
|
|
if self.deltas.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Compose all deltas
|
|
let mut composed = self.deltas[0].vector.clone();
|
|
for delta in self.deltas.iter().skip(1) {
|
|
composed = composed.compose(&delta.vector);
|
|
}
|
|
|
|
let first = self.deltas.first().unwrap();
|
|
let last = self.deltas.last().unwrap();
|
|
|
|
self.status = WindowStatus::Compacted;
|
|
|
|
Some(AggregatedDelta {
|
|
window_id: self.id,
|
|
source_id: self.source_id.clone(),
|
|
first_delta_id: first.id,
|
|
last_delta_id: last.id,
|
|
delta_count: self.deltas.len() as u32,
|
|
composed_vector: composed,
|
|
time_span: TimeSpan {
|
|
start: first.timestamp,
|
|
end: last.timestamp,
|
|
},
|
|
compression_ratio: self.compute_compression_ratio(&composed),
|
|
})
|
|
}
|
|
|
|
fn compute_compression_ratio(&self, composed: &DeltaVector) -> f32 {
|
|
let original_bytes: usize = self.deltas.iter()
|
|
.map(|d| d.vector.to_bytes().len())
|
|
.sum();
|
|
let composed_bytes = composed.to_bytes().len();
|
|
|
|
if composed_bytes > 0 {
|
|
original_bytes as f32 / composed_bytes as f32
|
|
} else {
|
|
1.0
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result of window compaction
|
|
#[derive(Clone, Debug)]
|
|
pub struct AggregatedDelta {
|
|
pub window_id: WindowId,
|
|
pub source_id: SourceId,
|
|
pub first_delta_id: DeltaId,
|
|
pub last_delta_id: DeltaId,
|
|
pub delta_count: u32,
|
|
pub composed_vector: DeltaVector,
|
|
pub time_span: TimeSpan,
|
|
pub compression_ratio: f32,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct TimeSpan {
|
|
pub start: DeltaTimestamp,
|
|
pub end: DeltaTimestamp,
|
|
}
|
|
|
|
/// Batch of deltas for bulk operations
|
|
#[derive(Clone, Debug)]
|
|
pub struct DeltaBatch {
|
|
pub id: BatchId,
|
|
pub deltas: Vec<Delta>,
|
|
pub created_at: u64,
|
|
pub checksum: DeltaChecksum,
|
|
}
|
|
|
|
impl DeltaBatch {
|
|
pub fn new(deltas: Vec<Delta>) -> Self {
|
|
let id = BatchId(rand::random());
|
|
|
|
// Compute batch checksum
|
|
let mut data = Vec::new();
|
|
for delta in &deltas {
|
|
data.extend(&delta.id.to_bytes());
|
|
}
|
|
let checksum = DeltaChecksum::compute(&data);
|
|
|
|
Self {
|
|
id,
|
|
deltas,
|
|
created_at: std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs(),
|
|
checksum,
|
|
}
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.deltas.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.deltas.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum WindowError {
|
|
WindowClosed,
|
|
PolicyViolation(String),
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Domain Events
|
|
|
|
| Event | Payload | Published When |
|
|
|-------|---------|----------------|
|
|
| `WindowOpened` | window_id, source_id, policy | New aggregation window started |
|
|
| `WindowClosed` | window_id, delta_count, duration_ms | Window reached boundary |
|
|
| `WindowCompacted` | window_id, compression_ratio | Deltas merged within window |
|
|
| `BatchCreated` | batch_id, delta_count, checksum | Batch assembled |
|
|
| `BatchCompressed` | batch_id, original_size, compressed_size | Batch compressed for storage |
|
|
| `AggregationPolicyChanged` | source_id, old_policy, new_policy | Window policy updated |
|
|
|
|
---
|
|
|
|
### 3.4 Delta Application Domain
|
|
|
|
**Purpose**: Apply deltas to target states with validation and transformation.
|
|
|
|
#### Ubiquitous Language
|
|
|
|
| Term | Definition |
|
|
|------|------------|
|
|
| **Applicator** | Component that applies deltas to target state |
|
|
| **Target** | State vector being modified by deltas |
|
|
| **Validator** | Component that verifies delta applicability |
|
|
| **Transformer** | Component that modifies deltas before application |
|
|
| **ApplicationResult** | Outcome of delta application (success/failure) |
|
|
| **Rollback** | Reverting applied deltas |
|
|
|
|
#### Aggregate Roots
|
|
|
|
```rust
|
|
/// Delta Application Domain
|
|
pub mod delta_application {
|
|
use super::delta_capture::*;
|
|
use std::collections::HashMap;
|
|
|
|
// ============================================================
|
|
// VALUE OBJECTS
|
|
// ============================================================
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
pub struct TargetId(pub u64);
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum ApplicationStatus {
|
|
Pending,
|
|
Applied,
|
|
Failed,
|
|
RolledBack,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum ValidationResult {
|
|
Valid,
|
|
Invalid { reason: String },
|
|
Warning { message: String },
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum ConflictResolution {
|
|
/// Last write wins
|
|
LastWriteWins,
|
|
/// First write wins
|
|
FirstWriteWins,
|
|
/// Merge by averaging
|
|
Merge,
|
|
/// Reject on conflict
|
|
Reject,
|
|
/// Custom resolution function
|
|
Custom,
|
|
}
|
|
|
|
// ============================================================
|
|
// AGGREGATES
|
|
// ============================================================
|
|
|
|
/// Target state that receives delta applications
|
|
#[derive(Clone, Debug)]
|
|
pub struct DeltaTarget {
|
|
pub id: TargetId,
|
|
pub source_id: SourceId,
|
|
/// Current state vector
|
|
pub state: Vec<f32>,
|
|
/// Last applied delta
|
|
pub last_delta_id: Option<DeltaId>,
|
|
/// Last application timestamp
|
|
pub last_applied_at: Option<DeltaTimestamp>,
|
|
/// Application policy
|
|
pub policy: ApplicationPolicy,
|
|
/// Application history (ring buffer)
|
|
pub history: ApplicationHistory,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ApplicationPolicy {
|
|
/// How to handle conflicts
|
|
pub conflict_resolution: ConflictResolution,
|
|
/// Maximum magnitude allowed per delta
|
|
pub max_magnitude: Option<f32>,
|
|
/// Dimensions that are read-only
|
|
pub locked_dimensions: Vec<u32>,
|
|
/// Whether to validate checksum chain
|
|
pub verify_chain: bool,
|
|
/// Maximum history entries to keep
|
|
pub history_limit: usize,
|
|
}
|
|
|
|
impl Default for ApplicationPolicy {
|
|
fn default() -> Self {
|
|
Self {
|
|
conflict_resolution: ConflictResolution::LastWriteWins,
|
|
max_magnitude: None,
|
|
locked_dimensions: vec![],
|
|
verify_chain: true,
|
|
history_limit: 1000,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ring buffer of recent applications
|
|
#[derive(Clone, Debug)]
|
|
pub struct ApplicationHistory {
|
|
pub entries: Vec<ApplicationEntry>,
|
|
pub capacity: usize,
|
|
pub head: usize,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ApplicationEntry {
|
|
pub delta_id: DeltaId,
|
|
pub applied_at: DeltaTimestamp,
|
|
pub status: ApplicationStatus,
|
|
/// Delta for rollback (inverted)
|
|
pub rollback_delta: Option<DeltaVector>,
|
|
}
|
|
|
|
impl ApplicationHistory {
|
|
pub fn new(capacity: usize) -> Self {
|
|
Self {
|
|
entries: Vec::with_capacity(capacity),
|
|
capacity,
|
|
head: 0,
|
|
}
|
|
}
|
|
|
|
pub fn push(&mut self, entry: ApplicationEntry) {
|
|
if self.entries.len() < self.capacity {
|
|
self.entries.push(entry);
|
|
} else {
|
|
self.entries[self.head] = entry;
|
|
}
|
|
self.head = (self.head + 1) % self.capacity;
|
|
}
|
|
|
|
pub fn last(&self) -> Option<&ApplicationEntry> {
|
|
if self.entries.is_empty() {
|
|
None
|
|
} else {
|
|
let idx = if self.head == 0 {
|
|
self.entries.len() - 1
|
|
} else {
|
|
self.head - 1
|
|
};
|
|
self.entries.get(idx)
|
|
}
|
|
}
|
|
|
|
/// Get entries for rollback (most recent first)
|
|
pub fn rollback_entries(&self, count: usize) -> Vec<&ApplicationEntry> {
|
|
let mut result = Vec::with_capacity(count);
|
|
let len = self.entries.len().min(count);
|
|
|
|
for i in 0..len {
|
|
let idx = if self.head >= i + 1 {
|
|
self.head - i - 1
|
|
} else {
|
|
self.entries.len() - (i + 1 - self.head)
|
|
};
|
|
if let Some(entry) = self.entries.get(idx) {
|
|
if entry.status == ApplicationStatus::Applied {
|
|
result.push(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
}
|
|
|
|
impl DeltaTarget {
|
|
pub fn new(
|
|
id: TargetId,
|
|
source_id: SourceId,
|
|
initial_state: Vec<f32>,
|
|
policy: ApplicationPolicy,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
source_id,
|
|
state: initial_state,
|
|
last_delta_id: None,
|
|
last_applied_at: None,
|
|
policy: policy.clone(),
|
|
history: ApplicationHistory::new(policy.history_limit),
|
|
}
|
|
}
|
|
|
|
/// Validate a delta before application
|
|
pub fn validate(&self, delta: &Delta) -> ValidationResult {
|
|
// Check source matches
|
|
if delta.source_id != self.source_id {
|
|
return ValidationResult::Invalid {
|
|
reason: "Source ID mismatch".to_string(),
|
|
};
|
|
}
|
|
|
|
// Check dimensions match
|
|
if delta.vector.total_dims as usize != self.state.len() {
|
|
return ValidationResult::Invalid {
|
|
reason: format!(
|
|
"Dimension mismatch: delta has {} dims, target has {}",
|
|
delta.vector.total_dims, self.state.len()
|
|
),
|
|
};
|
|
}
|
|
|
|
// Check magnitude limit
|
|
if let Some(max_mag) = self.policy.max_magnitude {
|
|
if delta.vector.magnitude.l2_norm > max_mag {
|
|
return ValidationResult::Invalid {
|
|
reason: format!(
|
|
"Magnitude {} exceeds limit {}",
|
|
delta.vector.magnitude.l2_norm, max_mag
|
|
),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check locked dimensions
|
|
for &locked_dim in &self.policy.locked_dimensions {
|
|
if delta.vector.indices.contains(&locked_dim) {
|
|
return ValidationResult::Invalid {
|
|
reason: format!("Dimension {} is locked", locked_dim),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check causal ordering (parent should be our last applied)
|
|
if self.policy.verify_chain {
|
|
if let Some(expected_parent) = self.last_delta_id {
|
|
if delta.parent_id != Some(expected_parent) {
|
|
return ValidationResult::Warning {
|
|
message: format!(
|
|
"Non-sequential delta: expected parent {:?}, got {:?}",
|
|
expected_parent, delta.parent_id
|
|
),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
ValidationResult::Valid
|
|
}
|
|
|
|
/// Apply a delta to the target state
|
|
pub fn apply(&mut self, delta: &Delta) -> Result<ApplicationResult, ApplicationError> {
|
|
// Validate first
|
|
match self.validate(delta) {
|
|
ValidationResult::Invalid { reason } => {
|
|
return Err(ApplicationError::ValidationFailed(reason));
|
|
}
|
|
ValidationResult::Warning { message } => {
|
|
// Log warning but continue
|
|
eprintln!("Warning: {}", message);
|
|
}
|
|
ValidationResult::Valid => {}
|
|
}
|
|
|
|
// Store rollback delta
|
|
let rollback_delta = delta.vector.invert();
|
|
|
|
// Apply the delta
|
|
delta.vector.apply(&mut self.state);
|
|
|
|
// Update metadata
|
|
self.last_delta_id = Some(delta.id);
|
|
self.last_applied_at = Some(delta.timestamp);
|
|
|
|
// Record in history
|
|
self.history.push(ApplicationEntry {
|
|
delta_id: delta.id,
|
|
applied_at: delta.timestamp,
|
|
status: ApplicationStatus::Applied,
|
|
rollback_delta: Some(rollback_delta),
|
|
});
|
|
|
|
Ok(ApplicationResult {
|
|
delta_id: delta.id,
|
|
target_id: self.id,
|
|
status: ApplicationStatus::Applied,
|
|
new_state_hash: self.compute_state_hash(),
|
|
})
|
|
}
|
|
|
|
/// Rollback the last N applied deltas
|
|
pub fn rollback(&mut self, count: usize) -> Result<Vec<DeltaId>, ApplicationError> {
|
|
let entries = self.history.rollback_entries(count);
|
|
|
|
if entries.is_empty() {
|
|
return Err(ApplicationError::NothingToRollback);
|
|
}
|
|
|
|
let mut rolled_back = Vec::with_capacity(entries.len());
|
|
|
|
for entry in entries {
|
|
if let Some(ref rollback_delta) = entry.rollback_delta {
|
|
rollback_delta.apply(&mut self.state);
|
|
rolled_back.push(entry.delta_id);
|
|
}
|
|
}
|
|
|
|
// Update last_delta_id to the one before rollback
|
|
self.last_delta_id = self.history.entries
|
|
.iter()
|
|
.filter(|e| e.status == ApplicationStatus::Applied && !rolled_back.contains(&e.delta_id))
|
|
.last()
|
|
.map(|e| e.delta_id);
|
|
|
|
Ok(rolled_back)
|
|
}
|
|
|
|
fn compute_state_hash(&self) -> [u8; 32] {
|
|
use sha2::{Sha256, Digest};
|
|
let mut hasher = Sha256::new();
|
|
for val in &self.state {
|
|
hasher.update(&val.to_le_bytes());
|
|
}
|
|
let result = hasher.finalize();
|
|
let mut hash = [0u8; 32];
|
|
hash.copy_from_slice(&result);
|
|
hash
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ApplicationResult {
|
|
pub delta_id: DeltaId,
|
|
pub target_id: TargetId,
|
|
pub status: ApplicationStatus,
|
|
pub new_state_hash: [u8; 32],
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum ApplicationError {
|
|
ValidationFailed(String),
|
|
TargetNotFound(TargetId),
|
|
ConflictDetected { delta_id: DeltaId, reason: String },
|
|
NothingToRollback,
|
|
StateCurrupted(String),
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Domain Events
|
|
|
|
| Event | Payload | Published When |
|
|
|-------|---------|----------------|
|
|
| `DeltaApplied` | delta_id, target_id, new_state_hash | Delta successfully applied |
|
|
| `DeltaRejected` | delta_id, target_id, reason | Delta failed validation |
|
|
| `DeltaConflictDetected` | delta_id, conflicting_delta_id | Concurrent modification detected |
|
|
| `DeltaMerged` | delta_ids, merged_delta_id | Conflict resolved by merging |
|
|
| `DeltaRolledBack` | delta_ids, target_id | Deltas reverted |
|
|
| `TargetStateCorrupted` | target_id, expected_hash, actual_hash | Integrity check failed |
|
|
|
|
---
|
|
|
|
### 3.5 Delta Versioning Domain
|
|
|
|
**Purpose**: Manage temporal ordering, history, branching, and state reconstruction.
|
|
|
|
#### Ubiquitous Language
|
|
|
|
| Term | Definition |
|
|
|------|------------|
|
|
| **DeltaStream** | Linear sequence of causally-ordered deltas |
|
|
| **DeltaGraph** | DAG of deltas supporting branches and merges |
|
|
| **Snapshot** | Full state capture at a specific version |
|
|
| **Branch** | Named divergence from main delta stream |
|
|
| **Merge** | Combining two branches into one |
|
|
| **Replay** | Reconstructing state by applying deltas from a point |
|
|
|
|
#### Aggregate Roots
|
|
|
|
```rust
|
|
/// Delta Versioning Domain
|
|
pub mod delta_versioning {
|
|
use super::delta_capture::*;
|
|
use super::delta_aggregation::*;
|
|
use std::collections::{HashMap, HashSet, BTreeMap};
|
|
|
|
// ============================================================
|
|
// VALUE OBJECTS
|
|
// ============================================================
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
pub struct StreamId(pub u64);
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
pub struct SnapshotId(pub u64);
|
|
|
|
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
|
pub struct BranchId(pub String);
|
|
|
|
/// Version identifier (sequence number in stream)
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct Version(pub u64);
|
|
|
|
impl Version {
|
|
pub fn next(&self) -> Self {
|
|
Self(self.0 + 1)
|
|
}
|
|
|
|
pub fn genesis() -> Self {
|
|
Self(0)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// AGGREGATES
|
|
// ============================================================
|
|
|
|
/// Linear delta stream (append-only log)
|
|
#[derive(Clone, Debug)]
|
|
pub struct DeltaStream {
|
|
pub id: StreamId,
|
|
pub source_id: SourceId,
|
|
/// Deltas indexed by version
|
|
pub deltas: BTreeMap<Version, Delta>,
|
|
/// Current head version
|
|
pub head: Version,
|
|
/// Periodic snapshots for fast replay
|
|
pub snapshots: HashMap<Version, SnapshotId>,
|
|
/// Snapshot interval
|
|
pub snapshot_interval: u64,
|
|
/// Stream metadata
|
|
pub metadata: StreamMetadata,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct StreamMetadata {
|
|
pub created_at: u64,
|
|
pub last_updated: u64,
|
|
pub total_deltas: u64,
|
|
pub total_bytes: u64,
|
|
}
|
|
|
|
impl DeltaStream {
|
|
pub fn new(id: StreamId, source_id: SourceId, snapshot_interval: u64) -> Self {
|
|
Self {
|
|
id,
|
|
source_id,
|
|
deltas: BTreeMap::new(),
|
|
head: Version::genesis(),
|
|
snapshots: HashMap::new(),
|
|
snapshot_interval,
|
|
metadata: StreamMetadata::default(),
|
|
}
|
|
}
|
|
|
|
/// Append a delta to the stream
|
|
pub fn append(&mut self, delta: Delta) -> Version {
|
|
let version = self.head.next();
|
|
self.head = version;
|
|
|
|
self.metadata.total_deltas += 1;
|
|
self.metadata.total_bytes += delta.vector.to_bytes().len() as u64;
|
|
self.metadata.last_updated = delta.timestamp.physical;
|
|
|
|
self.deltas.insert(version, delta);
|
|
version
|
|
}
|
|
|
|
/// Get delta at specific version
|
|
pub fn get(&self, version: Version) -> Option<&Delta> {
|
|
self.deltas.get(&version)
|
|
}
|
|
|
|
/// Get delta range (inclusive)
|
|
pub fn range(&self, from: Version, to: Version) -> Vec<&Delta> {
|
|
self.deltas.range(from..=to)
|
|
.map(|(_, d)| d)
|
|
.collect()
|
|
}
|
|
|
|
/// Find nearest snapshot before version
|
|
pub fn nearest_snapshot(&self, version: Version) -> Option<(Version, SnapshotId)> {
|
|
self.snapshots.iter()
|
|
.filter(|(v, _)| **v <= version)
|
|
.max_by_key(|(v, _)| *v)
|
|
.map(|(v, s)| (*v, *s))
|
|
}
|
|
|
|
/// Check if snapshot is due
|
|
pub fn should_snapshot(&self) -> bool {
|
|
let last_snapshot_version = self.snapshots.keys().max().copied()
|
|
.unwrap_or(Version::genesis());
|
|
|
|
self.head.0 - last_snapshot_version.0 >= self.snapshot_interval
|
|
}
|
|
|
|
/// Record a snapshot
|
|
pub fn record_snapshot(&mut self, version: Version, snapshot_id: SnapshotId) {
|
|
self.snapshots.insert(version, snapshot_id);
|
|
}
|
|
}
|
|
|
|
/// DAG-based delta graph (supports branching)
|
|
#[derive(Clone, Debug)]
|
|
pub struct DeltaGraph {
|
|
pub source_id: SourceId,
|
|
/// All deltas by ID
|
|
pub nodes: HashMap<DeltaId, DeltaGraphNode>,
|
|
/// Child relationships (parent -> children)
|
|
pub edges: HashMap<DeltaId, Vec<DeltaId>>,
|
|
/// Named branches
|
|
pub branches: HashMap<BranchId, DeltaId>,
|
|
/// Main branch head
|
|
pub main_head: Option<DeltaId>,
|
|
/// Root deltas (no parent)
|
|
pub roots: HashSet<DeltaId>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct DeltaGraphNode {
|
|
pub delta: Delta,
|
|
pub version: Version,
|
|
pub branch: Option<BranchId>,
|
|
pub is_merge: bool,
|
|
/// Second parent for merge commits
|
|
pub merge_parent: Option<DeltaId>,
|
|
}
|
|
|
|
impl DeltaGraph {
|
|
pub fn new(source_id: SourceId) -> Self {
|
|
Self {
|
|
source_id,
|
|
nodes: HashMap::new(),
|
|
edges: HashMap::new(),
|
|
branches: HashMap::new(),
|
|
main_head: None,
|
|
roots: HashSet::new(),
|
|
}
|
|
}
|
|
|
|
/// Add a delta to the graph
|
|
pub fn add(&mut self, delta: Delta, branch: Option<BranchId>) -> DeltaId {
|
|
let delta_id = delta.id;
|
|
let parent_id = delta.parent_id;
|
|
|
|
// Determine version
|
|
let version = match parent_id {
|
|
Some(pid) => self.nodes.get(&pid)
|
|
.map(|n| n.version.next())
|
|
.unwrap_or(Version(1)),
|
|
None => Version(1),
|
|
};
|
|
|
|
// Create node
|
|
let node = DeltaGraphNode {
|
|
delta,
|
|
version,
|
|
branch: branch.clone(),
|
|
is_merge: false,
|
|
merge_parent: None,
|
|
};
|
|
|
|
self.nodes.insert(delta_id, node);
|
|
|
|
// Update edges
|
|
if let Some(pid) = parent_id {
|
|
self.edges.entry(pid).or_default().push(delta_id);
|
|
} else {
|
|
self.roots.insert(delta_id);
|
|
}
|
|
|
|
// Update branch head
|
|
if let Some(ref b) = branch {
|
|
self.branches.insert(b.clone(), delta_id);
|
|
} else {
|
|
self.main_head = Some(delta_id);
|
|
}
|
|
|
|
delta_id
|
|
}
|
|
|
|
/// Create a branch from a delta
|
|
pub fn create_branch(&mut self, branch_id: BranchId, from_delta: DeltaId) -> Result<(), VersioningError> {
|
|
if !self.nodes.contains_key(&from_delta) {
|
|
return Err(VersioningError::DeltaNotFound(from_delta));
|
|
}
|
|
|
|
if self.branches.contains_key(&branch_id) {
|
|
return Err(VersioningError::BranchExists(branch_id));
|
|
}
|
|
|
|
self.branches.insert(branch_id, from_delta);
|
|
Ok(())
|
|
}
|
|
|
|
/// Merge two branches
|
|
pub fn merge(
|
|
&mut self,
|
|
source_branch: &BranchId,
|
|
target_branch: &BranchId,
|
|
merged_vector: DeltaVector,
|
|
timestamp: DeltaTimestamp,
|
|
) -> Result<DeltaId, VersioningError> {
|
|
let source_head = self.branches.get(source_branch)
|
|
.ok_or_else(|| VersioningError::BranchNotFound(source_branch.clone()))?;
|
|
let target_head = self.branches.get(target_branch)
|
|
.ok_or_else(|| VersioningError::BranchNotFound(target_branch.clone()))?;
|
|
|
|
// Create merge delta
|
|
let merge_delta = Delta::new(
|
|
self.source_id.clone(),
|
|
timestamp,
|
|
self.nodes.get(target_head).map(|n| &n.delta),
|
|
merged_vector,
|
|
HashMap::from([("merge".to_string(), "true".to_string())]),
|
|
);
|
|
|
|
let merge_id = merge_delta.id;
|
|
|
|
// Add merge node
|
|
let version = self.nodes.get(target_head)
|
|
.map(|n| n.version.next())
|
|
.unwrap_or(Version(1));
|
|
|
|
let node = DeltaGraphNode {
|
|
delta: merge_delta,
|
|
version,
|
|
branch: Some(target_branch.clone()),
|
|
is_merge: true,
|
|
merge_parent: Some(*source_head),
|
|
};
|
|
|
|
self.nodes.insert(merge_id, node);
|
|
|
|
// Update edges (merge has two parents)
|
|
self.edges.entry(*target_head).or_default().push(merge_id);
|
|
self.edges.entry(*source_head).or_default().push(merge_id);
|
|
|
|
// Update target branch head
|
|
self.branches.insert(target_branch.clone(), merge_id);
|
|
|
|
Ok(merge_id)
|
|
}
|
|
|
|
/// Get ancestry path from root to delta
|
|
pub fn ancestry(&self, delta_id: DeltaId) -> Vec<DeltaId> {
|
|
let mut path = Vec::new();
|
|
let mut current = Some(delta_id);
|
|
|
|
while let Some(id) = current {
|
|
path.push(id);
|
|
current = self.nodes.get(&id)
|
|
.and_then(|n| n.delta.parent_id);
|
|
}
|
|
|
|
path.reverse();
|
|
path
|
|
}
|
|
|
|
/// Find common ancestor of two deltas
|
|
pub fn common_ancestor(&self, a: DeltaId, b: DeltaId) -> Option<DeltaId> {
|
|
let ancestry_a: HashSet<_> = self.ancestry(a).into_iter().collect();
|
|
|
|
for ancestor in self.ancestry(b) {
|
|
if ancestry_a.contains(&ancestor) {
|
|
return Some(ancestor);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Get all deltas in topological order
|
|
pub fn topological_order(&self) -> Vec<DeltaId> {
|
|
let mut result = Vec::new();
|
|
let mut visited = HashSet::new();
|
|
let mut stack: Vec<DeltaId> = self.roots.iter().copied().collect();
|
|
|
|
while let Some(id) = stack.pop() {
|
|
if visited.contains(&id) {
|
|
continue;
|
|
}
|
|
visited.insert(id);
|
|
result.push(id);
|
|
|
|
if let Some(children) = self.edges.get(&id) {
|
|
for &child in children {
|
|
if !visited.contains(&child) {
|
|
stack.push(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
}
|
|
|
|
/// Full state snapshot for fast replay
|
|
#[derive(Clone, Debug)]
|
|
pub struct DeltaSnapshot {
|
|
pub id: SnapshotId,
|
|
pub source_id: SourceId,
|
|
/// Full state vector at this point
|
|
pub state: Vec<f32>,
|
|
/// Delta that produced this state
|
|
pub delta_id: DeltaId,
|
|
/// Stream version (if from stream)
|
|
pub version: Version,
|
|
/// Timestamp of snapshot
|
|
pub created_at: DeltaTimestamp,
|
|
/// State checksum
|
|
pub checksum: DeltaChecksum,
|
|
}
|
|
|
|
impl DeltaSnapshot {
|
|
pub fn new(
|
|
source_id: SourceId,
|
|
state: Vec<f32>,
|
|
delta_id: DeltaId,
|
|
version: Version,
|
|
timestamp: DeltaTimestamp,
|
|
) -> Self {
|
|
let mut data = Vec::new();
|
|
for val in &state {
|
|
data.extend(&val.to_le_bytes());
|
|
}
|
|
let checksum = DeltaChecksum::compute(&data);
|
|
|
|
Self {
|
|
id: SnapshotId(rand::random()),
|
|
source_id,
|
|
state,
|
|
delta_id,
|
|
version,
|
|
created_at: timestamp,
|
|
checksum,
|
|
}
|
|
}
|
|
|
|
/// Verify snapshot integrity
|
|
pub fn verify(&self) -> bool {
|
|
let mut data = Vec::new();
|
|
for val in &self.state {
|
|
data.extend(&val.to_le_bytes());
|
|
}
|
|
let computed = DeltaChecksum::compute(&data);
|
|
computed == self.checksum
|
|
}
|
|
}
|
|
|
|
/// Index for efficient delta lookup
|
|
#[derive(Clone, Debug)]
|
|
pub struct DeltaIndex {
|
|
/// Delta by ID
|
|
by_id: HashMap<DeltaId, Delta>,
|
|
/// Deltas by source
|
|
by_source: HashMap<SourceId, Vec<DeltaId>>,
|
|
/// Deltas by time range
|
|
by_time: BTreeMap<u64, Vec<DeltaId>>,
|
|
/// Checksum -> Delta mapping
|
|
by_checksum: HashMap<DeltaChecksum, DeltaId>,
|
|
}
|
|
|
|
impl DeltaIndex {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
by_id: HashMap::new(),
|
|
by_source: HashMap::new(),
|
|
by_time: BTreeMap::new(),
|
|
by_checksum: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Index a delta
|
|
pub fn insert(&mut self, delta: Delta) {
|
|
let id = delta.id;
|
|
let source = delta.source_id.clone();
|
|
let time = delta.timestamp.physical;
|
|
let checksum = delta.checksum;
|
|
|
|
self.by_source.entry(source).or_default().push(id);
|
|
self.by_time.entry(time).or_default().push(id);
|
|
self.by_checksum.insert(checksum, id);
|
|
self.by_id.insert(id, delta);
|
|
}
|
|
|
|
/// Lookup by ID
|
|
pub fn get(&self, id: &DeltaId) -> Option<&Delta> {
|
|
self.by_id.get(id)
|
|
}
|
|
|
|
/// Query by source
|
|
pub fn by_source(&self, source: &SourceId) -> Vec<&Delta> {
|
|
self.by_source.get(source)
|
|
.map(|ids| ids.iter().filter_map(|id| self.by_id.get(id)).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Query by time range
|
|
pub fn by_time_range(&self, start_ms: u64, end_ms: u64) -> Vec<&Delta> {
|
|
self.by_time.range(start_ms..=end_ms)
|
|
.flat_map(|(_, ids)| ids.iter())
|
|
.filter_map(|id| self.by_id.get(id))
|
|
.collect()
|
|
}
|
|
|
|
/// Verify by checksum
|
|
pub fn verify(&self, checksum: &DeltaChecksum) -> Option<&Delta> {
|
|
self.by_checksum.get(checksum)
|
|
.and_then(|id| self.by_id.get(id))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum VersioningError {
|
|
DeltaNotFound(DeltaId),
|
|
BranchNotFound(BranchId),
|
|
BranchExists(BranchId),
|
|
SnapshotNotFound(SnapshotId),
|
|
ReplayFailed(String),
|
|
MergeConflict { delta_a: DeltaId, delta_b: DeltaId },
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Domain Events
|
|
|
|
| Event | Payload | Published When |
|
|
|-------|---------|----------------|
|
|
| `DeltaVersioned` | delta_id, version, stream_id | Delta assigned version number |
|
|
| `SnapshotCreated` | snapshot_id, version, state_hash | Full state captured |
|
|
| `BranchCreated` | branch_id, from_delta_id | New branch started |
|
|
| `BranchMerged` | source_branch, target_branch, merge_delta_id | Branches combined |
|
|
| `StreamCompacted` | stream_id, before_count, after_count | Old deltas archived |
|
|
| `ReplayStarted` | from_version, to_version | State reconstruction begun |
|
|
| `ReplayCompleted` | target_version, delta_count, duration_ms | State reconstruction finished |
|
|
|
|
---
|
|
|
|
## 4. Bounded Context Map
|
|
|
|
```
|
|
+-----------------------------------------------------------------------+
|
|
| CONTEXT MAP |
|
|
+-----------------------------------------------------------------------+
|
|
|
|
+-----------------+
|
|
| Delta Capture |
|
|
| Context |
|
|
| (Core Domain) |
|
|
+--------+--------+
|
|
|
|
|
| [Published Language: Delta, DeltaVector, DeltaTimestamp]
|
|
|
|
|
v
|
|
+-----------------+ +-----------------+
|
|
| Delta |<------>| Delta |
|
|
| Propagation | ACL | Aggregation |
|
|
| Context | | Context |
|
|
| (Supporting) | | (Supporting) |
|
|
+--------+--------+ +--------+--------+
|
|
| |
|
|
| [ACL] | [Shared Kernel: DeltaWindow]
|
|
| |
|
|
v v
|
|
+-----------------+ +-----------------+
|
|
| Delta |<------>| Delta |
|
|
| Application | P/S | Versioning |
|
|
| Context | | Context |
|
|
| (Core Domain) | | (Core Domain) |
|
|
+-----------------+ +-----------------+
|
|
|
|
|
|
LEGEND:
|
|
[P/S] = Partnership (bidirectional cooperation)
|
|
[ACL] = Anti-Corruption Layer
|
|
[Published Language] = Shared vocabulary, immutable contracts
|
|
[Shared Kernel] = Co-owned code/types
|
|
|
|
INTEGRATION PATTERNS:
|
|
|
|
| Upstream | Downstream | Pattern | Shared Types |
|
|
|------------------|-------------------|--------------------|---------------------------------|
|
|
| Delta Capture | Propagation | Published Language | Delta, DeltaVector, DeltaId |
|
|
| Delta Capture | Aggregation | Published Language | Delta, DeltaTimestamp |
|
|
| Propagation | Application | ACL | RoutingDecision -> ApplyRequest |
|
|
| Aggregation | Application | Shared Kernel | AggregatedDelta |
|
|
| Aggregation | Versioning | Shared Kernel | DeltaWindow, BatchId |
|
|
| Application | Versioning | Partnership | ApplicationResult <-> Version |
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Anti-Corruption Layers
|
|
|
|
### 5.1 Propagation to Application ACL
|
|
|
|
```rust
|
|
/// ACL: Translate propagation concepts to application domain
|
|
pub mod propagation_to_application_acl {
|
|
use super::delta_propagation::*;
|
|
use super::delta_application::*;
|
|
use super::delta_capture::*;
|
|
|
|
/// Adapter that translates routing decisions into application requests
|
|
pub struct RoutingToApplicationAdapter;
|
|
|
|
impl RoutingToApplicationAdapter {
|
|
/// Convert a delivered delta into an application request
|
|
pub fn to_apply_request(
|
|
delta: &Delta,
|
|
routing: &RoutingDecision,
|
|
target_id: TargetId,
|
|
) -> ApplyRequest {
|
|
ApplyRequest {
|
|
delta: delta.clone(),
|
|
target_id,
|
|
priority: match routing.priority {
|
|
RoutingPriority::Critical => ApplicationPriority::Immediate,
|
|
RoutingPriority::High => ApplicationPriority::High,
|
|
RoutingPriority::Normal => ApplicationPriority::Normal,
|
|
RoutingPriority::Low => ApplicationPriority::Background,
|
|
},
|
|
timeout_ms: routing.ttl_ms,
|
|
retry_policy: match routing.priority {
|
|
RoutingPriority::Critical => RetryPolicy::Infinite,
|
|
RoutingPriority::High => RetryPolicy::Count(5),
|
|
RoutingPriority::Normal => RetryPolicy::Count(3),
|
|
RoutingPriority::Low => RetryPolicy::None,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Map application result back to acknowledgment
|
|
pub fn to_acknowledgment(
|
|
result: &ApplicationResult,
|
|
subscriber_id: SubscriberId,
|
|
) -> DeltaAcknowledgment {
|
|
DeltaAcknowledgment {
|
|
delta_id: result.delta_id,
|
|
subscriber_id,
|
|
success: matches!(result.status, ApplicationStatus::Applied),
|
|
new_version: Some(result.new_state_hash),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ApplyRequest {
|
|
pub delta: Delta,
|
|
pub target_id: TargetId,
|
|
pub priority: ApplicationPriority,
|
|
pub timeout_ms: Option<u64>,
|
|
pub retry_policy: RetryPolicy,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub enum ApplicationPriority {
|
|
Immediate,
|
|
High,
|
|
Normal,
|
|
Background,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub enum RetryPolicy {
|
|
None,
|
|
Count(u32),
|
|
Infinite,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct DeltaAcknowledgment {
|
|
pub delta_id: DeltaId,
|
|
pub subscriber_id: SubscriberId,
|
|
pub success: bool,
|
|
pub new_version: Option<[u8; 32]>,
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5.2 Aggregation to Versioning ACL
|
|
|
|
```rust
|
|
/// ACL: Translate aggregation windows to versioning streams
|
|
pub mod aggregation_to_versioning_acl {
|
|
use super::delta_aggregation::*;
|
|
use super::delta_versioning::*;
|
|
use super::delta_capture::*;
|
|
|
|
/// Adapter for converting aggregated deltas to stream entries
|
|
pub struct AggregationToVersioningAdapter {
|
|
snapshot_threshold: u32,
|
|
}
|
|
|
|
impl AggregationToVersioningAdapter {
|
|
pub fn new(snapshot_threshold: u32) -> Self {
|
|
Self { snapshot_threshold }
|
|
}
|
|
|
|
/// Convert a window's deltas into stream entries
|
|
pub fn window_to_stream_entries(
|
|
&self,
|
|
window: &DeltaWindow,
|
|
stream: &mut DeltaStream,
|
|
) -> Vec<Version> {
|
|
let mut versions = Vec::new();
|
|
|
|
for delta in &window.deltas {
|
|
let version = stream.append(delta.clone());
|
|
versions.push(version);
|
|
}
|
|
|
|
versions
|
|
}
|
|
|
|
/// Convert aggregated delta to single stream entry
|
|
pub fn aggregated_to_stream_entry(
|
|
&self,
|
|
aggregated: &AggregatedDelta,
|
|
stream: &mut DeltaStream,
|
|
timestamp: DeltaTimestamp,
|
|
) -> (Version, bool) {
|
|
// Create a synthetic delta from the aggregated vector
|
|
let parent = stream.get(stream.head);
|
|
|
|
let delta = Delta::new(
|
|
aggregated.source_id.clone(),
|
|
timestamp,
|
|
parent,
|
|
aggregated.composed_vector.clone(),
|
|
std::collections::HashMap::from([
|
|
("aggregated".to_string(), "true".to_string()),
|
|
("delta_count".to_string(), aggregated.delta_count.to_string()),
|
|
("compression_ratio".to_string(),
|
|
aggregated.compression_ratio.to_string()),
|
|
]),
|
|
);
|
|
|
|
let version = stream.append(delta);
|
|
let should_snapshot = stream.should_snapshot();
|
|
|
|
(version, should_snapshot)
|
|
}
|
|
|
|
/// Determine if window warrants a snapshot
|
|
pub fn should_create_snapshot(
|
|
&self,
|
|
window: &DeltaWindow,
|
|
current_version: Version,
|
|
) -> bool {
|
|
window.metrics.delta_count >= self.snapshot_threshold ||
|
|
window.metrics.total_magnitude > 10.0
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Repository Interfaces
|
|
|
|
```rust
|
|
/// Repository interfaces for persistence
|
|
pub mod repositories {
|
|
use super::delta_capture::*;
|
|
use super::delta_versioning::*;
|
|
use super::delta_aggregation::*;
|
|
use async_trait::async_trait;
|
|
|
|
// ============================================================
|
|
// DELTA REPOSITORY
|
|
// ============================================================
|
|
|
|
#[async_trait]
|
|
pub trait DeltaRepository: Send + Sync {
|
|
/// Store a delta
|
|
async fn save(&self, delta: &Delta) -> Result<(), RepositoryError>;
|
|
|
|
/// Retrieve delta by ID
|
|
async fn find_by_id(&self, id: &DeltaId) -> Result<Option<Delta>, RepositoryError>;
|
|
|
|
/// Find deltas by source
|
|
async fn find_by_source(
|
|
&self,
|
|
source_id: &SourceId,
|
|
limit: usize,
|
|
offset: usize,
|
|
) -> Result<Vec<Delta>, RepositoryError>;
|
|
|
|
/// Find deltas in time range
|
|
async fn find_by_time_range(
|
|
&self,
|
|
source_id: &SourceId,
|
|
start_ms: u64,
|
|
end_ms: u64,
|
|
) -> Result<Vec<Delta>, RepositoryError>;
|
|
|
|
/// Find deltas by parent (for graph traversal)
|
|
async fn find_children(&self, parent_id: &DeltaId) -> Result<Vec<Delta>, RepositoryError>;
|
|
|
|
/// Verify checksum chain
|
|
async fn verify_chain(
|
|
&self,
|
|
from_id: &DeltaId,
|
|
to_id: &DeltaId,
|
|
) -> Result<bool, RepositoryError>;
|
|
|
|
/// Bulk insert deltas
|
|
async fn save_batch(&self, deltas: &[Delta]) -> Result<usize, RepositoryError>;
|
|
|
|
/// Delete deltas older than version (for compaction)
|
|
async fn delete_before(
|
|
&self,
|
|
source_id: &SourceId,
|
|
before_timestamp: u64,
|
|
) -> Result<u64, RepositoryError>;
|
|
}
|
|
|
|
// ============================================================
|
|
// SNAPSHOT REPOSITORY
|
|
// ============================================================
|
|
|
|
#[async_trait]
|
|
pub trait SnapshotRepository: Send + Sync {
|
|
/// Store a snapshot
|
|
async fn save(&self, snapshot: &DeltaSnapshot) -> Result<(), RepositoryError>;
|
|
|
|
/// Retrieve snapshot by ID
|
|
async fn find_by_id(&self, id: &SnapshotId) -> Result<Option<DeltaSnapshot>, RepositoryError>;
|
|
|
|
/// Find nearest snapshot before version
|
|
async fn find_nearest(
|
|
&self,
|
|
source_id: &SourceId,
|
|
before_version: Version,
|
|
) -> Result<Option<DeltaSnapshot>, RepositoryError>;
|
|
|
|
/// List snapshots for source
|
|
async fn list_by_source(
|
|
&self,
|
|
source_id: &SourceId,
|
|
) -> Result<Vec<DeltaSnapshot>, RepositoryError>;
|
|
|
|
/// Delete old snapshots (keep N most recent)
|
|
async fn cleanup(
|
|
&self,
|
|
source_id: &SourceId,
|
|
keep_count: usize,
|
|
) -> Result<u64, RepositoryError>;
|
|
}
|
|
|
|
// ============================================================
|
|
// STREAM REPOSITORY
|
|
// ============================================================
|
|
|
|
#[async_trait]
|
|
pub trait StreamRepository: Send + Sync {
|
|
/// Get or create stream
|
|
async fn get_or_create(
|
|
&self,
|
|
source_id: &SourceId,
|
|
) -> Result<DeltaStream, RepositoryError>;
|
|
|
|
/// Save stream metadata
|
|
async fn save_metadata(
|
|
&self,
|
|
stream: &DeltaStream,
|
|
) -> Result<(), RepositoryError>;
|
|
|
|
/// Append delta to stream
|
|
async fn append(
|
|
&self,
|
|
stream_id: &StreamId,
|
|
delta: &Delta,
|
|
) -> Result<Version, RepositoryError>;
|
|
|
|
/// Get deltas in version range
|
|
async fn get_range(
|
|
&self,
|
|
stream_id: &StreamId,
|
|
from: Version,
|
|
to: Version,
|
|
) -> Result<Vec<Delta>, RepositoryError>;
|
|
|
|
/// Get current head version
|
|
async fn get_head(&self, stream_id: &StreamId) -> Result<Version, RepositoryError>;
|
|
}
|
|
|
|
// ============================================================
|
|
// INDEX REPOSITORY (for search)
|
|
// ============================================================
|
|
|
|
#[async_trait]
|
|
pub trait IndexRepository: Send + Sync {
|
|
/// Index a delta
|
|
async fn index(&self, delta: &Delta) -> Result<(), RepositoryError>;
|
|
|
|
/// Search by checksum
|
|
async fn search_by_checksum(
|
|
&self,
|
|
checksum: &DeltaChecksum,
|
|
) -> Result<Option<DeltaId>, RepositoryError>;
|
|
|
|
/// Search by metadata
|
|
async fn search_by_metadata(
|
|
&self,
|
|
key: &str,
|
|
value: &str,
|
|
) -> Result<Vec<DeltaId>, RepositoryError>;
|
|
|
|
/// Full-text search in metadata
|
|
async fn search_text(
|
|
&self,
|
|
query: &str,
|
|
limit: usize,
|
|
) -> Result<Vec<DeltaId>, RepositoryError>;
|
|
}
|
|
|
|
// ============================================================
|
|
// ERROR TYPES
|
|
// ============================================================
|
|
|
|
#[derive(Debug)]
|
|
pub enum RepositoryError {
|
|
NotFound(String),
|
|
Conflict(String),
|
|
ConnectionError(String),
|
|
SerializationError(String),
|
|
IntegrityError(String),
|
|
StorageFull,
|
|
Timeout,
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Event Flow Diagram
|
|
|
|
```
|
|
DELTA-BEHAVIOR EVENT FLOW
|
|
+----------------------------------------------------------------------------+
|
|
| |
|
|
| Source State Change |
|
|
| | |
|
|
| v |
|
|
| +-------------+ |
|
|
| | Observer |-----> [ChangeDetected] |
|
|
| +------+------+ |
|
|
| | |
|
|
| v |
|
|
| +-------------+ |
|
|
| | Extractor |-----> [DeltaExtracted] |
|
|
| +------+------+ |
|
|
| | |
|
|
| | Delta |
|
|
| v |
|
|
| +-------------+ +---------------+ |
|
|
| | Router |------->| Channel Pub |-----> [DeltaRouted] |
|
|
| +------+------+ +-------+-------+ |
|
|
| | | |
|
|
| | v |
|
|
| | +---------------+ |
|
|
| | | Subscriber |-----> [DeltaDelivered] |
|
|
| | +-------+-------+ |
|
|
| | | |
|
|
| v | |
|
|
| +-------------+ | |
|
|
| | Window | | |
|
|
| | Aggregator |-----> [WindowClosed, WindowCompacted] |
|
|
| +------+------+ | |
|
|
| | | |
|
|
| | AggregatedDelta | Delta |
|
|
| | | |
|
|
| v v |
|
|
| +-------------+ +-------------+ |
|
|
| | Versioning |<------->| Application | |
|
|
| | Stream | | Target | |
|
|
| +------+------+ +------+------+ |
|
|
| | | |
|
|
| | | |
|
|
| v v |
|
|
| [DeltaVersioned] [DeltaApplied] |
|
|
| [SnapshotCreated] [DeltaRolledBack] |
|
|
| [BranchCreated] [DeltaConflictDetected] |
|
|
| [BranchMerged] |
|
|
| |
|
|
+----------------------------------------------------------------------------+
|
|
```
|
|
|
|
---
|
|
|
|
## 8. WASM Integration Architecture
|
|
|
|
```rust
|
|
/// WASM bindings for Delta-Behavior system
|
|
pub mod wasm_bindings {
|
|
use wasm_bindgen::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
// ============================================================
|
|
// WASM-OPTIMIZED TYPES (minimal footprint)
|
|
// ============================================================
|
|
|
|
/// Compact delta for WASM (minimized size)
|
|
#[wasm_bindgen]
|
|
#[derive(Clone)]
|
|
pub struct WasmDelta {
|
|
id_high: u64,
|
|
id_low: u64,
|
|
parent_high: u64,
|
|
parent_low: u64,
|
|
timestamp: u64,
|
|
indices: Vec<u32>,
|
|
values: Vec<f32>,
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
impl WasmDelta {
|
|
#[wasm_bindgen(constructor)]
|
|
pub fn new(timestamp: u64, indices: Vec<u32>, values: Vec<f32>) -> Self {
|
|
let id = uuid::Uuid::new_v4().as_u128();
|
|
Self {
|
|
id_high: (id >> 64) as u64,
|
|
id_low: id as u64,
|
|
parent_high: 0,
|
|
parent_low: 0,
|
|
timestamp,
|
|
indices,
|
|
values,
|
|
}
|
|
}
|
|
|
|
/// Set parent delta ID
|
|
pub fn set_parent(&mut self, high: u64, low: u64) {
|
|
self.parent_high = high;
|
|
self.parent_low = low;
|
|
}
|
|
|
|
/// Get delta ID as hex string
|
|
pub fn id_hex(&self) -> String {
|
|
format!("{:016x}{:016x}", self.id_high, self.id_low)
|
|
}
|
|
|
|
/// Apply to a vector (in-place)
|
|
pub fn apply(&self, vector: &mut [f32]) {
|
|
for (&idx, &val) in self.indices.iter().zip(self.values.iter()) {
|
|
if (idx as usize) < vector.len() {
|
|
vector[idx as usize] += val;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Compute L2 magnitude
|
|
pub fn magnitude(&self) -> f32 {
|
|
self.values.iter().map(|v| v * v).sum::<f32>().sqrt()
|
|
}
|
|
|
|
/// Serialize to bytes
|
|
pub fn to_bytes(&self) -> Vec<u8> {
|
|
let mut bytes = Vec::with_capacity(
|
|
16 + 16 + 8 + 4 + self.indices.len() * 4 + self.values.len() * 4
|
|
);
|
|
|
|
bytes.extend(&self.id_high.to_le_bytes());
|
|
bytes.extend(&self.id_low.to_le_bytes());
|
|
bytes.extend(&self.parent_high.to_le_bytes());
|
|
bytes.extend(&self.parent_low.to_le_bytes());
|
|
bytes.extend(&self.timestamp.to_le_bytes());
|
|
bytes.extend(&(self.indices.len() as u32).to_le_bytes());
|
|
|
|
for &idx in &self.indices {
|
|
bytes.extend(&idx.to_le_bytes());
|
|
}
|
|
for &val in &self.values {
|
|
bytes.extend(&val.to_le_bytes());
|
|
}
|
|
|
|
bytes
|
|
}
|
|
|
|
/// Deserialize from bytes
|
|
pub fn from_bytes(bytes: &[u8]) -> Result<WasmDelta, JsValue> {
|
|
if bytes.len() < 44 {
|
|
return Err(JsValue::from_str("Buffer too small"));
|
|
}
|
|
|
|
let id_high = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
|
|
let id_low = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
|
|
let parent_high = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
|
|
let parent_low = u64::from_le_bytes(bytes[24..32].try_into().unwrap());
|
|
let timestamp = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
|
|
let count = u32::from_le_bytes(bytes[40..44].try_into().unwrap()) as usize;
|
|
|
|
let expected_len = 44 + count * 8;
|
|
if bytes.len() < expected_len {
|
|
return Err(JsValue::from_str("Buffer too small for data"));
|
|
}
|
|
|
|
let mut indices = Vec::with_capacity(count);
|
|
let mut values = Vec::with_capacity(count);
|
|
|
|
let mut offset = 44;
|
|
for _ in 0..count {
|
|
indices.push(u32::from_le_bytes(bytes[offset..offset+4].try_into().unwrap()));
|
|
offset += 4;
|
|
}
|
|
for _ in 0..count {
|
|
values.push(f32::from_le_bytes(bytes[offset..offset+4].try_into().unwrap()));
|
|
offset += 4;
|
|
}
|
|
|
|
Ok(Self {
|
|
id_high,
|
|
id_low,
|
|
parent_high,
|
|
parent_low,
|
|
timestamp,
|
|
indices,
|
|
values,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// WASM DELTA STREAM
|
|
// ============================================================
|
|
|
|
#[wasm_bindgen]
|
|
pub struct WasmDeltaStream {
|
|
deltas: Vec<WasmDelta>,
|
|
head_idx: Option<usize>,
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
impl WasmDeltaStream {
|
|
#[wasm_bindgen(constructor)]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
deltas: Vec::new(),
|
|
head_idx: None,
|
|
}
|
|
}
|
|
|
|
/// Append delta to stream
|
|
pub fn append(&mut self, mut delta: WasmDelta) -> usize {
|
|
// Set parent to current head
|
|
if let Some(head) = self.head_idx {
|
|
let parent = &self.deltas[head];
|
|
delta.set_parent(parent.id_high, parent.id_low);
|
|
}
|
|
|
|
let idx = self.deltas.len();
|
|
self.deltas.push(delta);
|
|
self.head_idx = Some(idx);
|
|
idx
|
|
}
|
|
|
|
/// Get delta count
|
|
pub fn len(&self) -> usize {
|
|
self.deltas.len()
|
|
}
|
|
|
|
/// Apply all deltas to a vector
|
|
pub fn apply_all(&self, vector: &mut [f32]) {
|
|
for delta in &self.deltas {
|
|
delta.apply(vector);
|
|
}
|
|
}
|
|
|
|
/// Apply deltas from index to head
|
|
pub fn apply_from(&self, start_idx: usize, vector: &mut [f32]) {
|
|
for delta in self.deltas.iter().skip(start_idx) {
|
|
delta.apply(vector);
|
|
}
|
|
}
|
|
|
|
/// Compact stream by composing deltas
|
|
pub fn compact(&mut self) -> WasmDelta {
|
|
let mut indices_map = std::collections::HashMap::new();
|
|
|
|
for delta in &self.deltas {
|
|
for (&idx, &val) in delta.indices.iter().zip(delta.values.iter()) {
|
|
*indices_map.entry(idx).or_insert(0.0f32) += val;
|
|
}
|
|
}
|
|
|
|
let mut sorted: Vec<_> = indices_map.into_iter().collect();
|
|
sorted.sort_by_key(|(idx, _)| *idx);
|
|
|
|
let indices: Vec<u32> = sorted.iter().map(|(i, _)| *i).collect();
|
|
let values: Vec<f32> = sorted.iter().map(|(_, v)| *v).collect();
|
|
|
|
let timestamp = self.deltas.last()
|
|
.map(|d| d.timestamp)
|
|
.unwrap_or(0);
|
|
|
|
WasmDelta::new(timestamp, indices, values)
|
|
}
|
|
|
|
/// Serialize entire stream
|
|
pub fn to_bytes(&self) -> Vec<u8> {
|
|
let mut bytes = Vec::new();
|
|
bytes.extend(&(self.deltas.len() as u32).to_le_bytes());
|
|
|
|
for delta in &self.deltas {
|
|
let delta_bytes = delta.to_bytes();
|
|
bytes.extend(&(delta_bytes.len() as u32).to_le_bytes());
|
|
bytes.extend(&delta_bytes);
|
|
}
|
|
|
|
bytes
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// WASM DELTA DETECTOR (Change detection in WASM)
|
|
// ============================================================
|
|
|
|
#[wasm_bindgen]
|
|
pub struct WasmDeltaDetector {
|
|
threshold: f32,
|
|
min_component_diff: f32,
|
|
last_state: Option<Vec<f32>>,
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
impl WasmDeltaDetector {
|
|
#[wasm_bindgen(constructor)]
|
|
pub fn new(threshold: f32, min_component_diff: f32) -> Self {
|
|
Self {
|
|
threshold,
|
|
min_component_diff,
|
|
last_state: None,
|
|
}
|
|
}
|
|
|
|
/// Detect delta between last state and new state
|
|
pub fn detect(&mut self, new_state: Vec<f32>) -> Option<WasmDelta> {
|
|
let delta = match &self.last_state {
|
|
Some(old) => {
|
|
if old.len() != new_state.len() {
|
|
return None;
|
|
}
|
|
|
|
let mut indices = Vec::new();
|
|
let mut values = Vec::new();
|
|
let mut magnitude_sq = 0.0f32;
|
|
|
|
for (i, (&old_val, &new_val)) in old.iter().zip(new_state.iter()).enumerate() {
|
|
let diff = new_val - old_val;
|
|
if diff.abs() > self.min_component_diff {
|
|
indices.push(i as u32);
|
|
values.push(diff);
|
|
magnitude_sq += diff * diff;
|
|
}
|
|
}
|
|
|
|
let magnitude = magnitude_sq.sqrt();
|
|
if magnitude < self.threshold {
|
|
None
|
|
} else {
|
|
let timestamp = js_sys::Date::now() as u64;
|
|
Some(WasmDelta::new(timestamp, indices, values))
|
|
}
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
self.last_state = Some(new_state);
|
|
delta
|
|
}
|
|
|
|
/// Reset detector state
|
|
pub fn reset(&mut self) {
|
|
self.last_state = None;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Technology Evaluation Matrix
|
|
|
|
| Component | Option A | Option B | Recommendation | Rationale |
|
|
|-----------|----------|----------|----------------|-----------|
|
|
| **Delta Storage** | PostgreSQL + JSONB | RuVector + Append Log | RuVector | Native vector support, HNSW for similarity |
|
|
| **Checksum Chain** | SHA-256 | Blake3 | Blake3 | 3x faster, streaming support |
|
|
| **Serialization** | JSON | Bincode | Bincode (WASM), JSON (API) | Size: 60% smaller, speed: 10x faster |
|
|
| **Timestamp** | Wall Clock | Hybrid Logical | Hybrid Logical | Causality without clock sync |
|
|
| **Conflict Resolution** | LWW | Vector Clocks | Vector Clocks | Concurrent detection |
|
|
| **WASM Runtime** | wasm-bindgen | wit-bindgen | wasm-bindgen | Mature, browser-compatible |
|
|
| **Pub/Sub** | Redis Streams | NATS | NATS (prod), in-process (embed) | Persistence + at-least-once |
|
|
| **Graph Storage** | Neo4j | ruvector-graph | ruvector-graph | Native integration |
|
|
|
|
---
|
|
|
|
## 10. Consequences
|
|
|
|
### Benefits
|
|
|
|
1. **Incremental Efficiency**: Only transmit/store actual changes (typically 1-5% of full vector)
|
|
2. **Temporal Queries**: Reconstruct any historical state via delta replay
|
|
3. **Conflict Visibility**: Concurrent modifications explicitly tracked and resolved
|
|
4. **Audit Trail**: Complete, tamper-evident history of all changes
|
|
5. **WASM Portability**: Core delta logic runs anywhere (browser, edge, server)
|
|
6. **Composability**: Deltas can be merged, compacted, or branched
|
|
7. **Clear Boundaries**: Each domain has explicit responsibilities
|
|
|
|
### Risks and Mitigations
|
|
|
|
| Risk | Impact | Probability | Mitigation |
|
|
|------|--------|-------------|------------|
|
|
| Replay performance at scale | High | Medium | Periodic snapshots (every N deltas) |
|
|
| Checksum chain corruption | High | Low | Redundant storage, verification on read |
|
|
| Window aggregation data loss | Medium | Low | WAL before window close |
|
|
| Branch merge conflicts | Medium | Medium | Clear resolution strategies per domain |
|
|
| WASM memory limits | Medium | Medium | Streaming delta application |
|
|
|
|
### Trade-offs
|
|
|
|
1. **Storage vs Replay Speed**: More frequent snapshots = faster replay, more storage
|
|
2. **Granularity vs Overhead**: Fine-grained deltas = better precision, more metadata overhead
|
|
3. **Compression vs Latency**: Window compaction = smaller storage, delayed visibility
|
|
4. **Consistency vs Availability**: Strict ordering = stronger guarantees, potential blocking
|
|
|
|
---
|
|
|
|
## 11. Implementation Roadmap
|
|
|
|
### Phase 1: Core Domain (4 weeks)
|
|
- [ ] Delta Capture domain implementation
|
|
- [ ] Value objects: DeltaId, DeltaTimestamp, DeltaVector
|
|
- [ ] WASM bindings for core types
|
|
- [ ] Unit tests for delta composition/inversion
|
|
|
|
### Phase 2: Propagation + Aggregation (3 weeks)
|
|
- [ ] Channel/Subscriber infrastructure
|
|
- [ ] Window aggregation with policies
|
|
- [ ] Routing rules engine
|
|
- [ ] Integration tests
|
|
|
|
### Phase 3: Application + Versioning (4 weeks)
|
|
- [ ] Delta application with validation
|
|
- [ ] Rollback support
|
|
- [ ] Version stream management
|
|
- [ ] Snapshot creation/restoration
|
|
- [ ] Branch/merge support
|
|
|
|
### Phase 4: Repositories + Integration (3 weeks)
|
|
- [ ] PostgreSQL repository implementations
|
|
- [ ] RuVector index integration
|
|
- [ ] NATS pub/sub integration
|
|
- [ ] End-to-end tests
|
|
|
|
### Phase 5: Production Hardening (2 weeks)
|
|
- [ ] Performance benchmarks
|
|
- [ ] WASM size optimization
|
|
- [ ] Monitoring/metrics
|
|
- [ ] Documentation
|
|
|
|
---
|
|
|
|
## 12. References
|
|
|
|
- Evans, Eric. "Domain-Driven Design: Tackling Complexity in the Heart of Software" (2003)
|
|
- Vernon, Vaughn. "Implementing Domain-Driven Design" (2013)
|
|
- Kleppmann, Martin. "Designing Data-Intensive Applications" (2017) - Chapter 5: Replication
|
|
- RuVector Core: `/workspaces/ruvector/crates/ruvector-core`
|
|
- RuVector DAG: `/workspaces/ruvector/crates/ruvector-dag`
|
|
- RuVector Replication: `/workspaces/ruvector/crates/ruvector-replication/src/conflict.rs`
|
|
- ADR-CE-004: Signed Event Log
|
|
|
|
---
|
|
|
|
## Revision History
|
|
|
|
| Version | Date | Author | Changes |
|
|
|---------|------|--------|---------|
|
|
| 1.0 | 2026-01-28 | System Architecture Designer | Initial ADR |
|