Cohesive Systems logoCOHESIVE SYSTEMS

Search Cohesive Systems

Ready

Search Cohesive Systems

Find product pages, building blocks, technical articles, and graph definitions.

Cohesive.Transitions Guide

Getting Started

Define an entity shape, author a typed Transition, persist and compile its canonical document, then interpret one state change without performing I/O or committing storage.

Start with One Change

This guide models one AssignCarrier operation. A Load has a status and optional carrier. The Transition admits only draft Loads, rejects an empty carrier identity, applies a sparse two-field patch, and checks the resulting invariant.

The example deliberately stops at a non-committing decision. That boundary makes the first program useful in a test without hiding the storage and publication work a production adapter must perform.

Install

Add the current prerelease package to a .NET 10 project:

dotnet add package Cohesive.Transitions --prerelease

Import the namespaces used by this walkthrough:

using Cohesive.Execution;
using Cohesive.Model;
using Cohesive.Model.Serialization;
using Cohesive.Transitions.Authoring;
using Cohesive.Transitions.Compilation;
using Cohesive.Transitions.Execution;
using Cohesive.Transitions.IR;

The package is evolving through prereleases. Keep the authoring, persistence, and execution packages on the same published version.

Define the Entity Shape

Start with the domain types used by the Transition:

public enum LoadStatus
{
    Draft,
    Assigned
}
 
public enum AssignCarrierOutcome
{
    Assigned,
    NotDraft,
    InvalidCarrier
}
 
public sealed record AssignCarrierInput(string CarrierId);

Declare the entity fields. Entity authoring contributes shape and invariant semantics; it does not create a Transition registry.

public sealed class Load : Entity<Load>
{
    public Load()
    {
        Status = Field(nameof(Status), LoadStatus.Draft);
        CarrierId = Field<string?>(
            nameof(CarrierId),
            initialValue: null,
            configure: field => field.Optional());
    }
 
    public Field<LoadStatus> Status { get; }
 
    public Field<string?> CarrierId { get; }
}

Load.Define().Shape is the canonical observation shape used by Transition authoring. The CLR class is a typed declaration surface, not the required in-memory representation for every execution.

Author the Transition

Give the Transition a stable definition identity, semantic revision, root-body identity, and provenance:

var metadata = new TransitionAuthoringMetadata(
    new("transition/load/assign-carrier"),
    new("revision/1"),
    new("assign-carrier/body"),
    new ExecutionProvenance(
        new(TransitionAuthoring.Producer),
        new("src/domain/Load.cs"),
        DocumentOrigin.User),
    displayName: "Assign carrier");

Now author the legal change with typed expressions:

var authored = TransitionAuthoring.Create<
    Load, AssignCarrierInput, AssignCarrierOutcome>(
    Load.Define().Shape,
    metadata,
    transition =>
    {
        transition.Requires(
            new("assign-carrier/admit/draft"),
            (load, _) => load.Status == LoadStatus.Draft,
            (_, _) => AssignCarrierOutcome.NotDraft);
 
        transition.Choose(new("assign-carrier/validate"), choice => choice
            .Case(
                new("assign-carrier/valid"),
                (_, input) => input.CarrierId != "",
                valid => valid
                    .Set(
                        new("assign-carrier/set-carrier"),
                        load => load.CarrierId,
                        (_, input) => input.CarrierId)
                    .Set(
                        new("assign-carrier/set-status"),
                        load => load.Status,
                        LoadStatus.Assigned)
                    .Return(
                        new("assign-carrier/assigned"),
                        TransitionOutcomeDisposition.Applied,
                        AssignCarrierOutcome.Assigned))
            .Fallback(
                new("assign-carrier/invalid"),
                invalid => invalid.Return(
                    new("assign-carrier/rejected"),
                    TransitionOutcomeDisposition.DomainRejected,
                    AssignCarrierOutcome.InvalidCarrier)));
 
        transition.Invariant(
            new("assign-carrier/invariant/carrier-required"),
            load => load.Status != LoadStatus.Assigned ||
                    load.CarrierId != null);
    });

The callback is construction-time syntax. Its lambdas must lower into the portable expression language; arbitrary method calls, hidden I/O, captured runtime state, loops, and mutation are rejected during authoring.

Inspect and Persist the Document

Check structured validation before using the definition:

if (!authored.IsValid)
{
    foreach (var diagnostic in authored.Validation.Diagnostics)
        Console.Error.WriteLine(
            $"{diagnostic.Code}: {diagnostic.Message}");
 
    return;
}

The typed handle exposes the canonical document and its exact reference:

var document = authored.Document;
var reference = authored.Reference;
 
Console.WriteLine(reference.DefinitionId);
Console.WriteLine(reference.RevisionId);
Console.WriteLine(reference.Fingerprint.Value);

Persist the document rather than the builder callback or a compiled plan:

var json = ExecutionDefinitionJsonSerializer.Serialize(document);

Strict restoration checks schema compatibility, the closed node model, canonical ordering, and the semantic fingerprint. A consumer can restore this JSON without loading the assembly that authored it.

Compile the Exact Definition

Static compilation validates the document and derives an immutable executable plan:

var compilation = TransitionStaticCompiler.Compile(document);
 
if (!compilation.IsSuccessful)
{
    foreach (var diagnostic in compilation.Validation.Diagnostics)
        Console.Error.WriteLine(
            $"{diagnostic.Code}: {diagnostic.Message}");
 
    return;
}
 
var plan = compilation.Plan!;

The plan is fingerprint-affine. It indexes the canonical program and its requirements, but it does not replace the document as semantic authority or select a storage engine.

Interpret One Decision

Construct typed portable input and one coherent aggregate observation:

var input = PortableValue.Concrete(
    plan.Definition.Input,
    ObservationValue.FromObject(
        new AssignCarrierInput("carrier-7")));
 
var state = PortableValue.Concrete(
    plan.Definition.Observation,
    ObservationValue.FromObject(new
    {
        Status = LoadStatus.Draft,
        CarrierId = (string?)null
    }));

Run the deterministic reference interpreter:

var decision = TransitionReferenceInterpreter.DecideFullState(
    plan,
    new("assign-carrier/example-1"),
    input,
    state);
 
Console.WriteLine(decision.Kind);
// Applied
 
foreach (var patch in decision.Patch)
    Console.WriteLine($"{patch.Path}: {patch.After.Value}");

The same execution core accepts sparse observation entries when an adapter has acquired only the demanded fields. Sparse evaluation preserves the difference between an unobserved path and an explicit absent, null, unknown, failed, or concrete value.

Understand the Result

The returned TransitionDecision keeps the semantic and execution evidence separate from infrastructure action:

Kind and Outcome

What it provides
Applied, no-change, admission rejection, domain rejection, conflict, invalid definition, or infrastructure failure with a typed outcome when available.

Patch

What it provides
Evaluated sparse algebraic updates, including before and after evidence for changed paths.

Emissions and Movements

What it provides
Pure interaction intents and actual linked Machine movements selected by this execution path.

ActualReads and Conflicts

What it provides
The exact observation paths consumed and any fresh-state mismatch found during commit validation.

GuaranteeDemands

What it provides
The coherence and atomic patch-plus-emission behavior an external commit interpretation must preserve.

Evidence and Diagnostics

What it provides
Ordered trace evidence and precise failures attributable to canonical nodes and source locations.

Commit Is an External Boundary

The reference interpreter has not updated a database or published a message. A Storage or Process integration must:

  1. acquire observations with enough completeness for the compiled requirements
  2. decide the Transition from that evidence
  3. validate fresh actual reads when concurrency requires it
  4. commit the accepted patch and required emissions with the demanded atomicity
  5. retain operation, trace, and publication evidence

Keeping that boundary visible prevents an in-memory callback from becoming accidental persistence authority.

Continue