Get started
Record two source histories, then compare what they saw.
The shortest useful path: define one state window, ingest events from two sources, compare the recorded windows, and inspect the output.
Mental model
Start with one question: when was this state active?
A window opens when your predicate becomes true for a key and source. It closes when the predicate becomes false. Once those windows are recorded, compare sources without hand-written interval joins.
1. Install
Add the package to your project.
dotnet add package Spanfold
cd packages/python
python -m pip install -e ".[dev]"
The Python package includes the core runtime, comparison, export, testing, and CLI surfaces.
cd packages/rust
cargo test --workspace
cargo run -p spanfold-cli -- --help
The Rust package includes the core runtime, selector-backed comparison API, deterministic exports, audit bundles, testing helpers, and CLI surfaces.
2. Minimal implementation
A complete first example.
This example uses processing positions rather than timestamps so the output is easy to inspect. Position 1 is the first ingested event, position 2 is the second, and so on.
using Spanfold;
var pipeline = Spanfold.Spanfold
.For<DeviceStatus>()
.RecordWindows()
.TrackWindow(
"DeviceOffline",
key: status => status.DeviceId,
isActive: status => !status.IsOnline);
Ingest("provider-a", isOnline: true); // position 1
Ingest("provider-b", isOnline: true); // position 2
Ingest("provider-a", isOnline: false); // position 3, provider-a opens
Ingest("provider-b", isOnline: false); // position 4, provider-b opens
Ingest("provider-b", isOnline: true); // position 5, provider-b closes
Ingest("provider-a", isOnline: true); // position 6, provider-a closes
var result = pipeline.History
.Compare("Provider comparison")
.Target("provider-a", selector => selector.Source("provider-a"))
.Against("provider-b", selector => selector.Source("provider-b"))
.Within(scope => scope.Window("DeviceOffline"))
.Using(comparators => comparators.Overlap().Residual().Missing())
.Run();
Console.WriteLine($"closed windows: {pipeline.History.ClosedWindows.Count}");
Console.WriteLine($"overlap rows: {result.OverlapRows.Count}");
Console.WriteLine($"provider-a-only rows: {result.ResidualRows.Count}");
Console.WriteLine($"provider-b-only rows: {result.MissingRows.Count}");
foreach (var row in result.OverlapRows)
{
Console.WriteLine($"overlap {row.Key}: {row.Range.Start.Position}..{row.Range.End!.Value.Position}");
}
foreach (var row in result.ResidualRows)
{
Console.WriteLine($"a-only {row.Key}: {row.Range.Start.Position}..{row.Range.End!.Value.Position}");
}
void Ingest(string source, bool isOnline)
{
pipeline.Ingest(new DeviceStatus("device-17", isOnline), source);
}
public sealed record DeviceStatus(string DeviceId, bool IsOnline);
from dataclasses import dataclass
from spanfold import Spanfold
@dataclass(frozen=True)
class DeviceStatus:
device_id: str
is_online: bool
pipeline = (
Spanfold.for_events()
.record_windows()
.track_window(
"DeviceOffline",
key=lambda status: status.device_id,
is_active=lambda status: not status.is_online,
)
)
pipeline.ingest(DeviceStatus("device-17", True), source="provider-a")
pipeline.ingest(DeviceStatus("device-17", True), source="provider-b")
pipeline.ingest(DeviceStatus("device-17", False), source="provider-a")
pipeline.ingest(DeviceStatus("device-17", False), source="provider-b")
pipeline.ingest(DeviceStatus("device-17", True), source="provider-b")
pipeline.ingest(DeviceStatus("device-17", True), source="provider-a")
result = (
pipeline.history.compare("Provider comparison")
.target("provider-a")
.against("provider-b")
.within(window_name="DeviceOffline")
.using("overlap", "residual", "missing")
.run()
)
print(f"closed windows: {len(pipeline.history.closed_windows)}")
print(f"overlap rows: {len(result.overlap_rows)}")
print(f"provider-a-only rows: {len(result.residual_rows)}")
print(f"provider-b-only rows: {len(result.missing_rows)}")
use spanfold::for_events;
#[derive(Clone)]
struct DeviceStatus {
device_id: String,
is_online: bool,
}
let mut pipeline = for_events::<DeviceStatus>()
.record_windows()
.track_window(
"DeviceOffline",
|status| status.device_id.clone(),
|status| !status.is_online,
)
.build();
fn status(is_online: bool) -> DeviceStatus {
DeviceStatus { device_id: "device-17".into(), is_online }
}
pipeline.ingest(status(true), Some("provider-a"), None); // position 1
pipeline.ingest(status(true), Some("provider-b"), None); // position 2
pipeline.ingest(status(false), Some("provider-a"), None); // position 3, provider-a opens
pipeline.ingest(status(false), Some("provider-b"), None); // position 4, provider-b opens
pipeline.ingest(status(true), Some("provider-b"), None); // position 5, provider-b closes
pipeline.ingest(status(true), Some("provider-a"), None); // position 6, provider-a closes
let result = pipeline
.history()
.compare("Provider comparison")
.target_source("provider-a")
.against_source("provider-b")
.scope_window("DeviceOffline")
.overlap()
.residual()
.missing()
.run();
println!("closed windows: {}", pipeline.history().closed_windows().len());
println!("overlap rows: {}", result.overlap_rows.len());
println!("provider-a-only rows: {}", result.residual_rows.len());
println!("provider-b-only rows: {}", result.missing_rows.len());
3. Expected output
The result should show one shared segment and two target-only segments.
closed windows: 2
overlap rows: 1
provider-a-only rows: 2
provider-b-only rows: 0
overlap device-17: 4..5
a-only device-17: 3..4
a-only device-17: 5..6
Provider A reported the device offline from position 3 to 6. Provider B reported it offline from position 4 to 5. Spanfold turns that into structured comparison rows: A-only before B saw it, overlap while both saw it, and A-only after B recovered.
4. What to try next