Guide · C# and Rust

Match a journey across named state windows.

Ordered sequences answer whether one key, source, and partition lane passed through literal named window families in order. The matcher is deliberately bounded: a linear list of steps, optional per-transition gaps, deterministic evidence selection, and no branching, negation, loops, or causal claim.

Choose the analytical unit

Use sequences for transitions between meanings.

A sequence step names a window family such as Warning, Offline, or Recovered. It does not describe a low-level event type or an arbitrary predicate over records. Record the interpreted states first, then ask whether a lane followed the named journey.

Exact comparisons

Use overlap, residual, missing, coverage, containment, or lead/lag rows when the result must explain precise agreement and disagreement between two selected histories.

Episodes

Use Episodes when nearby fragments of the same interpreted condition should form occurrences that are then related across target and against sides.

Ordered sequences

Use a sequence when different named conditions form a meaningful order within one lane: warning, outage, recovery; queued, processing, completed; or suspected, confirmed, resolved.

These views can be used together. A completed incident sequence says the three states occurred in order; it does not say two providers agreed on their exact ranges, or combine those windows into one occurrence.

Pattern and lane

Literal steps stay inside one correlation lane.

A plan has a non-blank analytical name, at least two literal window-family names, and an optional inclusive maximum processing-position gap. Repeating a family is valid: A → B → A requires separate records because one source record cannot fill two steps.

1. Anchor

The first family starts a candidate chain.

Only a record for the configured first step can anchor a completed match.

2. Correlate

Key, source, and partition must stay equal.

Evidence never crosses a device, provider, stage, or partition boundary to complete a journey.

3. Advance

Each next onset is not earlier.

Later steps may overlap earlier evidence; onset order, rather than non-overlap, defines the sequence.

4. Complete

One record fills every named step.

Only a complete chain becomes a match and consumes its contributing record IDs.

Rust lane identity uses exact string equality for key, source, and partition. In C#, the first step family's configured history key comparer anchors key equality for every later step; source and partition use exact object equality. This is a real native-model difference, so normalize key conventions before expecting cross-runtime agreement.

Deterministic matching

Earliest completion wins; committed evidence is not reused.

Within each lane, candidates for a step are ordered by effective end, then onset, then record ID. Starting from each first-step candidate, the matcher takes the first compatible unused candidate for every next step. It does not enumerate alternative chains or optimize for the largest number of possible matches.

RuleConsequence
Onset orderA candidate is eligible when its start is at or after the previous step's start. Overlapping steps are allowed.
Earliest completionThe lowest effective end wins first; onset and record ID resolve ties deterministically.
Commit on completionAn incomplete attempted chain consumes nothing, so its evidence may still support a later complete chain.
Non-reuseAfter a complete match commits, none of its source record IDs can contribute to another match in that lane.
Stable result orderCompleted matches are returned by end, then start, then contributing record IDs, with native lane identity used as an additional Rust tie-breaker.

Optional gap constraint

Limit inactivity between each consecutive pair.

For a transition, inactive gap is max(0, next start − previous effective end). An overlap therefore has gap zero. WithMaximumGap(...) / with_maximum_gap(...) applies the inclusive limit to every transition independently; it is not an end-to-end duration limit.

No maximum

Any later-onset record in the same lane can complete the next step, regardless of intervening inactivity.

Maximum of 5

A transition with gap 5 qualifies; gap 6 does not. Each pair in a three-step sequence is checked separately.

Reported total gap

The match sums the positive inactive gaps between selected steps. Its end-to-end magnitude is separately measured from the first onset to the latest effective end.

Direct library APIs

Build the same bounded question idiomatically.

The examples assume history already contains recorded Warning, Offline, and Recovered windows. Historical execution requires processing-position evidence and requires every record from the selected families in that history to be closed.

C# · current repository source

using Spanfold.Sequences;

var result = history
    .MatchSequence("incident journey")
    .Step("Warning")
    .Then("Offline")
    .Then("Recovered")
    .WithMaximumGap(5)
    .Run();

foreach (var match in result.Matches)
{
    Console.WriteLine(
        $"{match.Key}: {match.StartPosition}..{match.EndPosition} " +
        $"gap={match.TotalGapPositionMagnitude} {match.Finality}");

    foreach (var record in match.Evidence)
    {
        Console.WriteLine($"  {record.Window.WindowName} {record.Window.Id}");
    }
}

Rust · current repository source

let matches = history
    .match_sequence("incident journey")
    .step("Warning")
    .then("Offline")
    .then("Recovered")
    .with_maximum_gap(5)
    .run()?;

for sequence_match in &matches {
    println!(
        "{}: {}..{} gap={} {:?}",
        sequence_match.key(),
        sequence_match.start().magnitude(),
        sequence_match.end().magnitude(),
        sequence_match.total_gap(),
        sequence_match.finality(),
    );

    for record in sequence_match.evidence() {
        println!("  {} {}", record.window.window_name(), record.window.id());
    }
}

Builder validation follows each host language: C# rejects invalid calls as the fluent plan is built or run; Rust returns WindowSequenceError from execution.

Lineage and output

The match keeps the exact evidence that completed it.

Evidence is an ordered snapshot record per configured step. Each snapshot record retains the source WindowRecord and its record ID, plus the effective range and finality used by matching. Keep this native evidence when an audit must explain why a journey was accepted.

OutputC# / .NET sourceRust source
Execution resultWindowSequenceResult with Plan, ordered Matches, and optional EvaluationHorizon.Vec<WindowSequenceMatch>; there is no enclosing result object or stored evaluation horizon.
Plan nameAvailable through result.Plan.Name.Stored on every match and returned by name().
Lane identityKey, Source, and Partition are native objects.key() is a string; source() and partition() are optional strings.
Range and magnitudeStartPosition, EndPosition, EndToEndPositionMagnitude, and TotalGapPositionMagnitude.start(), end(), end_to_end_magnitude(), and total_gap().
LineageEvidence contains ordered WindowSnapshotRecord values with source window IDs.evidence() returns the same native snapshot-record shape.
Portable outputThere is currently no portable sequence plan/result document, deterministic cross-runtime sequence schema, or CLI sequence route.

Live horizon and finality

A live run matches only evidence visible at the horizon.

Use RunLive(TemporalPoint.ForPosition(horizon)) or run_live(TemporalPoint::position(horizon)). The history snapshot excludes future evidence, clips a still-open or not-yet-ended record to the horizon, and marks that snapshot record provisional.

C# · live source history

using Spanfold.Comparison;

var live = history
    .MatchSequence("incident journey")
    .Step("Warning")
    .Then("Offline")
    .Then("Recovered")
    .WithMaximumGap(5)
    .RunLive(TemporalPoint.ForPosition(42));

var provisional = live.Matches
    .Where(match => match.Finality == ComparisonFinality.Provisional);

Rust · live source history

let live = history
    .match_sequence("incident journey")
    .step("Warning")
    .then("Offline")
    .then("Recovered")
    .with_maximum_gap(5)
    .run_live(TemporalPoint::position(42))?;

let provisional = live.iter().filter(|sequence_match| {
    sequence_match.finality() == &ComparisonFinality::Provisional
});

Evidence-derived finality

A completed match is provisional exactly when any selected snapshot record is provisional. If every selected record is final, the match is final.

No gap-settling boundary

The maximum gap only tests transitions that were selected. It does not keep an otherwise final match provisional while waiting for another possible future candidate.

No absence result

A live run returns completed visible chains. It does not emit a provisional “not matched yet” row for a journey that future evidence might complete.

No completeness promise

The horizon does not imply a watermark, allowed lateness, retention policy, or source completeness. Callers own those guarantees and rerun matching when history changes.

Availability

The source APIs are ahead of both published packages.

Ordered sequences landed in the repository after the current published NuGet and crates.io baselines. Use a repository checkout until each ecosystem receives a later release that explicitly includes the feature.

RuntimeCurrent repository sourcePublished package
C# / .NETImplemented in source version 0.2.0-preview.1 under Spanfold.Sequences.NuGet.org Spanfold 0.1.0-preview.2 predates ordered sequences.
RustImplemented on the current branch under spanfold::sequences; the crate manifest still reads 0.1.1.The published crates.io spanfold 0.1.1 release predates ordered sequences despite sharing the source tree's current version number.

The native semantics intentionally correspond, but package versioning, result wrapping, key identity, and error handling remain runtime-specific. No portable document currently turns the sequence surface into a cross-runtime compatibility contract.

Next

Choose the neighboring view for the next question.

Read exact comparison semantics when range disagreement matters, use Episode analysis when fragments should form occurrences, or define live ordering and completeness ownership before publishing changing sequence results.