C# · published packages
dotnet add package Spanfold --version 0.1.0-preview.2
dotnet add package Spanfold.Testing --version 0.1.0-preview.2
dotnet tool install --global Spanfold.Cli --version 0.1.0-preview.2
C# ↔ Rust portability
Spanfold's C# and Rust APIs share the same model—record windows, select two sides, normalize, align, compare, then inspect or export evidence. This guide maps that workflow by task while preserving the idioms and package boundaries of each runtime.
Choose a runtime
Repository source is ahead of published packages in places. The current .NET source is ahead of NuGet.org, and ordered sequences landed after the crates.io 0.1.1 release, so check feature-level availability before porting a source example into an installed package.
| Concern | C# / .NET | Rust |
|---|---|---|
| Core library | Spanfold 0.1.0-preview.2 on NuGet.org; repository source is 0.2.0-preview.1. | spanfold 0.1.1 on crates.io. |
| Testing helpers | Spanfold.Testing 0.1.0-preview.2 is a separate NuGet package. | SpanfoldAssert, SpanfoldSnapshot, fixture builders, and the virtual clock ship in spanfold. |
| Exports and bundles | Spanfold.Artifacts joins Spanfold and Spanfold.Testing as a supported package in the repository's 0.2.0-preview.1 release set. | Deterministic export functions ship in spanfold; audit-bundle assembly is owned by spanfold-cli. |
| Command line | The supported Spanfold.Cli surface is checkout-only and runs through its source project; it is not published as a NuGet tool. | spanfold-cli 0.1.1 is published; its installed command is spanfold. |
dotnet add package Spanfold --version 0.1.0-preview.2
dotnet add package Spanfold.Testing --version 0.1.0-preview.2
dotnet tool install --global Spanfold.Cli --version 0.1.0-preview.2
cargo add spanfold@0.1.1
cargo install spanfold-cli --version 0.1.1
Task map
The nouns remain stable. C# uses properties, extension methods, and configuring delegates; Rust uses borrowed history, consuming builders, explicit values, and Result at fallible boundaries.
| Task | C# / .NET source | Rust 0.1.1 |
|---|---|---|
| Start a pipeline | EventPipeline.For<TEvent>() | for_events::<T>() |
| Record history | .RecordWindows() | .record_windows() |
| Add one window and finish | .TrackWindow(...) returns the pipeline | .track_window(...).build()? |
| Add several windows | .Window(...).Window(...).Build() | .window(...).window(...).build()? |
| Ingest one / many | Ingest(...) / IngestMany(...) | ingest(...)? / ingest_many(...)? |
| Borrow recorded history | pipeline.History | pipeline.history() |
| Adopt materialized history | Construct ClosedWindow/OpenWindow, then call WindowHistory.FromRecords(...) | Deserialize a validated WindowHistory, or use fixture/tool adapters; external crates cannot construct the non-exhaustive record structs directly |
| Query a lane | Query().Window(...).Lane(...).ClosedWindows() | query().where_window(...).where_source(...).closed_windows() |
| Snapshot at a horizon | SnapshotAt(TemporalPoint.ForPosition(...)) | snapshot_at(TemporalPoint::position(...))? |
| Start a comparison | history.Compare(name) | history.compare(name) |
| Select sides | Target(name, ...) / Against(name, ...) | target_selector(...) / against_selector(...), or source conveniences |
| Scope and normalize | Within(...) / Normalize(...) | scope(...) / normalization(...) |
| Choose rows | Using(c => c.Overlap().Residual()...) | overlap().residual()... |
| Validate / prepare / align | Validate() / Prepare() / prepared.Align() | validate() / prepare() / align() |
| Run final / live | Run() / RunLive(horizon) | run() / run_live(horizon) |
Record
In both runtimes, source and partition are ingestion context. Processing position advances once per committed event, and recording must be enabled before history-based analysis is useful.
var pipeline = EventPipeline
.For<DeviceStatus>()
.RecordWindows()
.TrackWindow(
"DeviceOffline",
e => e.DeviceId,
e => !e.IsOnline);
var ingestion = pipeline.Ingest(
new DeviceStatus("device-17", false),
source: "provider-a",
partition: "eu-west");
var history = pipeline.History;
let mut pipeline = spanfold::for_events::<DeviceStatus>()
.record_windows()
.track_window(
"DeviceOffline",
|e| e.device_id.clone(),
|e| !e.is_online,
)
.build()?;
let ingestion = pipeline.ingest(
DeviceStatus { device_id: "device-17".into(), is_online: false },
Some("provider-a"),
Some("eu-west"),
)?;
let history = pipeline.history();
The published .NET 0.1.0-preview.2 examples use Spanfold.Spanfold.For<TEvent>(). In current source, EventPipeline.For<TEvent>() is the preferred entry point and the older static entry point is obsolete.
For a batch with one Rust lane, ingest_many(events, source, partition) applies that context to every event. The C# IngestMany(events) overload has no lane parameters; call Ingest(event, source, partition) for each event when source or partition matters.
In current C# source, emission callbacks run after the event and its history have committed. An IngestionCallbackException<TEvent> reports callback failures and carries the committed result; do not blindly retry that event.
Inspect
Both APIs filter by window name, key, source, partition, segment, and tag. A snapshot clips evidence to an explicit horizon and labels records derived from still-open windows as provisional.
var closed = history.Query()
.Window("DeviceOffline")
.Key("device-17")
.Lane("provider-a")
.ClosedWindows();
var snapshot = history.SnapshotAt(
TemporalPoint.ForPosition(42));
var visible = snapshot.Query()
.Window("DeviceOffline")
.Windows();
let closed = history.query()
.where_window("DeviceOffline")
.where_key("device-17")
.where_source("provider-a")
.closed_windows();
let snapshot = history.snapshot_at(
TemporalPoint::position(42),
)?;
let visible = snapshot.query()
.where_window("DeviceOffline")
.windows();
Compare
Build captures the question. Validate critiques the plan. Prepare selects, normalizes, and records exclusions. Align partitions time into comparable segments. Run executes the requested comparators and materializes rows.
var comparison = history
.Compare("Provider QA")
.Target("primary", s => s.Source("provider-a"))
.Against("secondary", s => s.Source("provider-b"))
.Within(s => s.Window("DeviceOffline"))
.Using(c => c
.Overlap()
.Residual()
.Missing()
.Coverage());
var diagnostics = comparison.Validate();
var prepared = comparison.Prepare();
var aligned = prepared.Align();
var result = comparison.Run();
let comparison = history
.compare("Provider QA")
.target_selector(
ComparisonSelector::for_source("provider-a")
.with_name("primary"),
)
.against_selector(
ComparisonSelector::for_source("provider-b")
.with_name("secondary"),
)
.scope_window("DeviceOffline")
.overlap()
.residual()
.missing()
.coverage();
let diagnostics = comparison.validate();
let prepared = comparison.prepare();
let aligned = comparison.align();
let result = comparison.run();
Use serializable selectors when the plan will be exported. Rust also permits ComparisonSelector::runtime_only(...) for local predicates, but deterministic plan and result exports reject that plan as non-portable.
Consume evidence
Use the typed collections for analysis, then use the finality-aware views when a live horizon can make a row provisional. Finality describes horizon dependence; it is not a watermark or a claim that late records cannot arrive.
| Evidence | C# | Rust |
|---|---|---|
| Core rows | OverlapRows, ResidualRows, MissingRows | overlap_rows, residual_rows, missing_rows |
| Advanced rows | CoverageRows, GapRows, SymmetricDifferenceRows, ContainmentRows, LeadLagRows, AsOfRows | The same names in snake_case. |
| Finality-aware view | ResidualRowsWithFinality() | residual_rows_with_finality()? |
| Finality values | ComparisonFinality.Final and Provisional. | The non-exhaustive ComparisonFinality also includes Revised and Retracted. Handle unknown future variants rather than assuming only Final and Provisional. |
| Stage evidence | result.Prepared and result.Aligned are typed optional stage objects; diagnostics are typed in result.Diagnostics. | result.prepared and result.aligned are Option<serde_json::Value>, not direct typed counterparts; diagnostics remain in result.diagnostics. |
var live = comparison.RunLive(
TemporalPoint.ForPosition(42));
var provisional = live
.ResidualRowsWithFinality()
.Where(row =>
row.Metadata.Finality ==
ComparisonFinality.Provisional);let live = comparison.run_live(
TemporalPoint::position(42),
);
let provisional = live
.residual_rows_with_finality()?
.filter(|row|
row.metadata.finality ==
ComparisonFinality::Provisional);Export and audit
Both implementations emit deterministic JSON, JSON Lines, Markdown, debug HTML, and LLM-context output. Keep the comparison runtime free of implicit file writes: create a result first, then export it or explicitly choose a configured export path.
| Output | C# · Spanfold.Artifacts | Rust · spanfold |
|---|---|---|
| Plan JSON | plan.ExportJson() | export_plan_json(&plan)? |
| Result JSON | result.ExportJson() | export_result_json(&result)? |
| JSON Lines | result.ExportJsonLines() | export_result_json_lines(&result)? or write_result_json_lines(...)? |
| Markdown | result.ExportMarkdown() | export_result_markdown(&result) |
| Debug HTML | result.ExportDebugHtml(path) | export_result_debug_html(&result), or a configured builder export |
| LLM context | result.ExportLlmContext() | export_result_llm_context(&result)?, or a configured builder export |
| Audit bundle | AuditBundleWriter.Write(...); AuditBundleReader.Open(...).Verify() checks manifest integrity. | spanfold audit ..., audit-windows, or audit-events assemble CLI bundles. |
A .NET bundle verifier checks file sizes and SHA-256 digests against its manifest; it does not authenticate the producer. The Rust CLI bundle is a deterministic artifact collection, but it does not expose the .NET integrity-verification contract.
Occurrences and journeys
Episode analysis stitches nearby fragments within each side, then relates formed occurrences across sides. Sequences instead match literal named window families in onset order within one key/source/partition lane.
using Spanfold.Episodes;
var episodes = history
.CompareEpisodes("Provider QA")
.Target("reference", s => s.Source("provider-a"))
.Against("detector", s => s.Source("provider-b"))
.Within(s => s.Window("DeviceOffline"))
.StitchGapsUpTo(2L)
.RelateWithin(1L)
.Run();
var scorecard = episodes.AsReference();
let episodes = history
.compare_episodes("Provider QA")
.target(
"reference",
ComparisonSelector::for_source("provider-a"),
)
.against(
"detector",
ComparisonSelector::for_source("provider-b"),
)
.scope(ComparisonScope::window("DeviceOffline"))
.stitch_gaps_up_to(TemporalTolerance::processing_positions(2)?)
.relate_within(TemporalTolerance::processing_positions(1)?)
.run()?;
let scorecard = episodes.as_reference();
using Spanfold.Sequences;
var journeys = history
.MatchSequence("incident journey")
.Step("Warning")
.Then("Offline")
.Then("Recovered")
.WithMaximumGap(5)
.Run()
.Matches;
let journeys = history
.match_sequence("incident journey")
.step("Warning")
.then("Offline")
.then("Recovered")
.with_maximum_gap(5)
.run()?;
Portable Episode document schema version 1 is processing-position only. The .NET and Rust CLIs can execute the same document and emit aligned deterministic JSON or Markdown; runtime-specific Episode IDs are deliberately omitted from that portable result.
Ordered sequences are current-source APIs in both runtimes. They landed after the published NuGet 0.1.0-preview.2 and crates.io 0.1.1 packages, and there is no portable sequence document or CLI route.
Read Episode formation, graph, and portability semantics · Read sequence lane, matching, gap, lineage, and live semantics
Automation
The versioned contract fixture and Episode document are the portable boundaries. Assertion APIs are intentionally language-native and return or throw in the style of their host runtime.
| Workflow | C# / .NET source CLI | Rust 0.1.1 CLI |
|---|---|---|
| Shared fixture paths | validate-plan, compare, explain, audit, audit-windows, and episodes | |
| Event import | No CLI event-map import route. | import-events and audit-events accept mapped JSON Lines or header-row CSV. |
| Acceptance policies | check and suite execute assessment specifications. | No assessment-specification CLI route. |
| Bundle lifecycle | verify-bundle and diff. | No bundle verification or bundle-diff command. |
| Fixture builder | WindowHistoryFixtureBuilder | WindowHistoryFixture |
| Assertions | SpanfoldAssert throws SpanfoldAssertionException. | SpanfoldAssert returns Result<_, SpanfoldAssertionError>. |
| Snapshots and time | SpanfoldSnapshot, VirtualComparisonClock | The same type names with snake_case methods. |
dotnet run --project packages/dotnet/src/Spanfold.Cli/Spanfold.Cli.csproj -- \
compare fixture.json --format jsonspanfold compare fixture.json --format jsonRuntime differences and current gaps
NuGet.org currently publishes Spanfold and Spanfold.Testing 0.1.0-preview.2. The .NET 0.2.0-preview.1 release set adds Spanfold.Artifacts as a supported package; its CLI remains checkout-only. Rust library and CLI 0.1.1 are published.
Compare semantic rows, ranges, finality, and evidence lineage across runtimes. Current row IDs are runtime-specific, and portable Episode output intentionally excludes runtime Episode IDs.
Contract fixtures and portable Episode schema version 1 use processing positions. Portable timestamp-known-at input remains deferred until both runtimes share one explicit clock and availability contract.
Rust owns declarative event JSONL/CSV import. .NET owns assessment specifications, integrity-verifiable bundles, and artifact revision/diff workflows. These are supported runtime-specific capabilities, not missing aliases to invent in application code.
Neither runtime is a hosted stream processor, durable queue, scheduler, or persistence layer. Pipelines are synchronous and callers own IO, retention, ingestion serialization, and when a live comparison is rerun. The live stream operations guide maps that ownership into a safe end-to-end workflow; the existing-history guide covers adoption without event replay.
The main capability map is implemented in both runtimes, but broader cross-language conformance gates and published workload baselines remain incomplete. Treat deterministic shared documents and fixtures—not file layout or method spelling—as the compatibility surface.
Next
This guide is the translation layer. The dedicated references remain authoritative for overloads, errors, advanced comparators, cohorts, source matrices, hierarchy, liveness, and detailed result types.