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.
Cohesive.Transitions Guide
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.
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.
Add the current prerelease package to a .NET 10 project:
dotnet add package Cohesive.Transitions --prereleaseImport 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.
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.
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.
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.
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.
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.
The returned TransitionDecision keeps the semantic and execution evidence separate from infrastructure action:
The reference interpreter has not updated a database or published a message. A Storage or Process integration must:
Keeping that boundary visible prevents an in-memory callback from becoming accidental persistence authority.