Adopt existing history

Analyze the windows you already have.

If your system already materializes intervals, you do not need to replay raw events through a Spanfold pipeline. Map those intervals into a WindowHistory, then use the same query, snapshot, and comparison APIs as pipeline-recorded history.

Choose the boundary

Library adoption and CLI workflows solve different integration problems.

Use the library when your application already owns loading and mapping. Use fixture or flat-window files when a portable command-line workflow is the better boundary. Neither route makes Spanfold your database.

Application library

Load records from your store, map them to the runtime's history contract, keep the history in memory, and call query or comparison APIs directly.

Contract fixture

Use the versioned spanfold.contract-fixture JSON envelope when windows and a comparison plan should travel together for examples or regression contracts.

Flat window JSONL

Use spanfold audit-windows when each line is already one processing-position window and you want comparison artifacts without application code.

Raw event import

Rust's import-events and audit-events map event JSONL or CSV into windows. They are event-reconstruction tools, not the route for data that is already materialized.

Contract fixture and flat-window formats are tool contracts, not a promised persistence schema for WindowHistory. The Rust CLI owns event-map import; the .NET CLI does not provide an equivalent event-map route.

Map the evidence

Preserve the identity and temporal domain that make an interval meaningful.

ConcernC# / .NETRust
Record shapePublic ClosedWindow and OpenWindow constructors.ClosedWindow and OpenWindow are public but #[non_exhaustive] and have no public constructors.
Record identityWindowRecord.Id is derived deterministically from the record data; it is not supplied by the importer or a distributed global ID.Each record carries an explicit non-blank WindowRecordId.
RangeEvery record has non-negative processing positions. Optional DateTimeOffset bounds and a timestamp clock add an event-time view.A closed record owns one half-open TemporalRange; an open record owns one TemporalPoint. The chosen axis is processing position or opaque timestamp ticks.
Lane and scopeWindowName, object Key, optional object Source, and optional object Partition.Non-blank string window_name and key, plus optional non-blank string source and partition.
AvailabilityImported records do not carry a separate known-at field. A processing-position known-at comparison excludes records that start later and clips records still active at the horizon; annotations may carry their own KnownAt.Records can carry an explicit known_at point in the same temporal domain. When absent, closed records default to their end for known-at comparison; an open record defaults to its start.
ContextWindowSegment and WindowTag accept object values. Segments are analytical boundaries; tags are descriptive metadata.Segments and tags use JSON-compatible PrimitiveValues. Names must be unique within a record; a parent segment must appear before the child that names it.
Close evidenceClosed records may preserve a boundary reason and segment boundary changes.Closed records expose the equivalent optional boundary reason and boundary-change list.

Both temporal ranges are half-open: start is included and end is excluded. C# creates points with TemporalPoint.ForPosition(...) or ForTimestamp(...), then ranges with TemporalRange.Closed(...) or Open(...). Rust uses TemporalPoint::position(...), timestamp_ticks(...), or timestamp_ticks_with_clock(...), and validates closed ranges through TemporalRange::new(...) or positions(...).

Current source contract

The two library import surfaces are not symmetrical.

C# · public construction and import

public static WindowHistory FromRecords(
    IEnumerable<ClosedWindow> closedWindows,
    IEnumerable<OpenWindow> openWindows)

Both sequences are required. Record constructors validate their own window identity, key, position, timestamp, and clock fields. Import copies closed records and indexes open records by window name, key, source, partition, and segment context; a duplicate open key fails when that index is built.

Rust · public function, restricted record construction

pub fn from_records(
    closed: impl IntoIterator<Item = ClosedWindow>,
    open: impl IntoIterator<Item = OpenWindow>,
) -> Result<WindowHistory, WindowHistoryImportError>

The function rebuilds the open-record index and returns DuplicateOpenRecordId for a repeated open ID. However, external crates cannot construct the non-exhaustive record structs with literals, and no public constructors currently bridge that gap.

C# validation

Window names cannot be blank, keys cannot be null, positions cannot be negative, ends cannot precede starts, end timestamps require start timestamps, and timestamp clocks cannot be blank or exist without a start timestamp. WindowSegment, WindowTag, and WindowBoundaryChange metadata is not checked for blank or duplicate names, parent consistency, or equivalent domain rules; validate those fields in your adapter.

Rust validated deserialization

serde_json::from_str::<WindowHistory>(...) applies strict unknown-field and domain validation to the history and window-record payloads, including ranges, identity fields, known-at domains, segment/tag metadata, and duplicate open record IDs. Annotation structs are less strict: they do not deny unknown fields or apply the same name and temporal-domain validation. WindowHistory::annotate only appends and assigns a revision; even when using it, validate annotation names, targets, and known-at domains in your adapter.

What import does not validate

FromRecords/from_records is not a global deduplication or business-consistency pass. Closed-record duplication and overlap remain questions for your mapping policy or later analysis.

Annotations start separately

Both record-import functions create a history with no annotations. Add append-only annotations after import; importing records does not infer them from tags or replace the original window.

Until Rust exposes public record constructors or another typed adoption boundary, use validated WindowHistory deserialization for exact externally materialized records, or use WindowHistoryFixture/ContractFixture for processing-position fixture data. Do not copy private struct construction from crate tests into application code.

Paired provider example

Import equivalent closed windows, inspect one lane, then compare both.

Both examples represent provider A active over [10, 20) and provider B over [12, 22) for the same device and partition. They preserve one segment and one tag, query provider A, snapshot at position 25, and produce overlap, residual, and missing rows.

C# · construct records directly

using Spanfold;
using Spanfold.Comparison;

var providerARange = TemporalRange.Closed(
    TemporalPoint.ForPosition(10),
    TemporalPoint.ForPosition(20));
var providerBRange = TemporalRange.Closed(
    TemporalPoint.ForPosition(12),
    TemporalPoint.ForPosition(22));

var segments = new[] { new WindowSegment("region", "eu-west") };
var tags = new[] { new WindowTag("service", "checkout") };

var providerA = new ClosedWindow(
    "DeviceOffline", "device-17",
    providerARange.Start.Position, providerARange.End!.Value.Position,
    Source: "provider-a", Partition: "fleet-1",
    Segments: segments, Tags: tags);
var providerB = new ClosedWindow(
    "DeviceOffline", "device-17",
    providerBRange.Start.Position, providerBRange.End!.Value.Position,
    Source: "provider-b", Partition: "fleet-1",
    Segments: segments, Tags: tags);

var history = WindowHistory.FromRecords(
    new[] { providerA, providerB },
    Array.Empty<OpenWindow>());

var lane = history.Query()
    .Window("DeviceOffline")
    .Lane("provider-a")
    .Partition("fleet-1")
    .Segment("region", "eu-west")
    .ClosedWindows();

var snapshot = history.SnapshotAt(TemporalPoint.ForPosition(25));

var comparison = history.Compare("Provider QA")
    .Target("provider-a", side => side.Source("provider-a"))
    .Against("provider-b", side => side.Source("provider-b"))
    .Within(scope => scope.Window("DeviceOffline"))
    .Using(rows => rows.Overlap().Residual().Missing())
    .Run();

Rust · deserialize the validated history shape

use spanfold::{TemporalPoint, WindowHistory};

fn main() -> Result<(), Box<dyn std::error::Error>> {

    let json = r#"{
  "closed": [
    {
      "id": "provider-a-device-17-10",
      "window_name": "DeviceOffline",
      "key": "device-17",
      "range": {
        "start": { "axis": "ProcessingPosition", "magnitude": 10, "clock": null },
        "end": { "axis": "ProcessingPosition", "magnitude": 20, "clock": null }
      },
      "known_at": { "axis": "ProcessingPosition", "magnitude": 20, "clock": null },
      "source": "provider-a",
      "partition": "fleet-1",
      "segments": [{ "name": "region", "value": "eu-west", "parent_name": null }],
      "tags": [{ "name": "service", "value": "checkout" }],
      "boundary_reason": null,
      "boundary_changes": []
    },
    {
      "id": "provider-b-device-17-12",
      "window_name": "DeviceOffline",
      "key": "device-17",
      "range": {
        "start": { "axis": "ProcessingPosition", "magnitude": 12, "clock": null },
        "end": { "axis": "ProcessingPosition", "magnitude": 22, "clock": null }
      },
      "known_at": { "axis": "ProcessingPosition", "magnitude": 22, "clock": null },
      "source": "provider-b",
      "partition": "fleet-1",
      "segments": [{ "name": "region", "value": "eu-west", "parent_name": null }],
      "tags": [{ "name": "service", "value": "checkout" }],
      "boundary_reason": null,
      "boundary_changes": []
    }
  ],
  "open": [],
  "annotations": []
}"#;

    let history: WindowHistory = serde_json::from_str(json)?;

    let lane = history.query()
        .where_window("DeviceOffline")
        .where_source("provider-a")
        .where_partition("fleet-1")
        .where_segment("region", "eu-west")
        .closed_windows();

    let snapshot = history.snapshot_at(TemporalPoint::position(25))?;

    let comparison = history.compare("Provider QA")
        .target_source("provider-a")
        .against_source("provider-b")
        .scope_window("DeviceOffline")
        .overlap()
        .residual()
        .missing()
        .run();

    Ok(())
}

The Rust example depends on serde_json in the application. The serialized WindowHistory shape is the crate's current serde contract, not a cross-language or durable-storage schema promise. Put your own versioned DTO in front of it when long-lived compatibility matters.

Open records

Import open windows only when “still active” is genuinely known.

An open record is not a closed interval with a missing value. It is evidence whose end is still unknown. Keep its source, partition, segment context, start domain, and record identity stable.

Snapshot explicitly

SnapshotAt/snapshot_at clips records active at a compatible horizon and marks their snapshot records provisional without mutating the imported history.

Compare live explicitly

Final comparison rejects open windows by default. Use RunLive(horizon) in C# or run_live(horizon)/clip_open_windows_to_position(...) in Rust when provisional evidence belongs in the answer.

Do not guess an end

If the source cannot distinguish “still active” from “not yet refreshed,” keep that uncertainty in the source system. A fabricated close converts uncertainty into false final evidence.

Do not expect pipeline continuation

An imported history is an analysis snapshot. Importing an open record does not attach it to an event pipeline that will later close it.

Annotations

Attach later knowledge without rewriting the source interval.

Both runtimes support append-only annotations targeted at a stable window-start identity. Repeated names receive increasing revision numbers. Known-at queries include only annotations with a compatible point at or before the requested horizon; annotations without known-at are excluded from that point-in-time-safe view.

Tags are part of the imported window and can participate in query and comparison scope. Annotations are later external metadata. Do not use an annotation as a silent correction to the interval itself.

Correction and recovery

Your source remains authoritative; rebuild the analysis view when it changes.

Own mapping

Version the adapter from your domain records into Spanfold fields. Decide how source IDs become keys, lanes, partitions, segments, tags, clocks, and Rust record IDs.

Own deduplication

Resolve duplicate source rows and corrections before import. Spanfold can analyze overlaps; it does not decide which conflicting persisted row should survive.

Rebuild on correction

Create a new history from the corrected authoritative snapshot and rerun the query or comparison. There is no public merge, upsert, or retroactive window-rewrite API at this boundary.

Own persistence

Persist your source DTOs, checkpoints, and result versions outside Spanfold. The core history is in-memory and does not claim a database schema, migration path, repository, or distributed recovery protocol.