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.
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)}")
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