Yurai 0.1.0

dotnet add package Yurai --version 0.1.0
                    
NuGet\Install-Package Yurai -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Yurai" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Yurai" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Yurai" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Yurai --version 0.1.0
                    
#r "nuget: Yurai, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Yurai@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Yurai&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Yurai&version=0.1.0
                    
Install as a Cake Tool

Yurai

CI Deep quality Mutation gate netstandard2.0 Dependencies License: MIT

Yurai is a lightweight computation-lineage library for explainable domain calculations in .NET.

The name comes from the Japanese word 由来 (yurai), "origin" — how something came to be. Fitting, since that is exactly what the library keeps attached to every value it touches.

using System;
using Yurai;

var basePrice = Traced.Of(1000m, "BasePrice");
var discount = Traced.Of(0.10m, "MemberDiscount");
var taxRate = Traced.Of(0.10m, "TaxRate");
var total = (basePrice * (1 - discount) * (1 + taxRate))
    .Round(0, "Round to whole currency unit")
    .As("Total");

Console.WriteLine(total.Explain());
Result
  990
Derivation
  Total = 990
    Round(digits: 0, reason: "Round to whole currency unit") = 990
      Multiply = 990.0000
        Multiply = 900.00
          BasePrice = 1000
          Subtract = 0.90
            1
            MemberDiscount = 0.10
        Add = 1.10
          1
          TaxRate = 0.10

total is a value you can keep using — total.Value is 990m, exactly what the same expression produces without Yurai. What Yurai adds is that the value carries the evidence of how it was reached, and that evidence can be printed, exported as JSON, or queried in code.

Status: 0.1.0. The library is implemented and its behavior is under test — the output above is what it prints, pinned by a test that reads this page. The version is deliberately 0.x: the public surface may still change, and a second value type beside decimal is the change most likely to move it.

Try it: Follow the three-step Quick start to run this example in a new console application.

Why this exists

Domain calculations get questioned. A support ticket asks why an invoice says 990. A regulator asks which rate was applied. A colleague asks whether last month's fix changed the payroll formula. The usual answers are a debugger session, a log line that records the result but not the reasoning, or a comment in the code that may no longer be true.

Yurai keeps the reasoning attached to the value while the calculation runs, so the answer is available afterwards without re-running anything.

The four kinds of "why"

"Why is this value what it is?" is four different questions. Yurai answers two of them and deliberately refuses the other two, because a dependency path read as a sensitivity or an apportionment leads to a wrong business decision.

Question Example Yurai
Dependency — which inputs does this value depend on? Does NetPay depend on SocialInsuranceRate? Answered
Derivation — how was it computed from those inputs? Which operations ran, in what order, with what rounding and which branch? Answered
Sensitivity — how much would the result change if an input changed? What would Total be if the tax rate were 12%? Out of scope, permanently
Attribution — how much of the result is owed to a given input? How much of Total is owed to tax? Out of scope, permanently

In Yurai's vocabulary, a trace is the dependency path of a value. It is not an execution log, not a diagnostic trail, and not a statement about how much any input matters.

What Yurai records

  • Named inputs and named intermediate results. Traced.Of(1000m, "BasePrice") brings a value in under your domain vocabulary; .As("Total") names a result after computing it. Names are what the explanation and the queries speak in.
  • Ordinary arithmetic. + - * / between traced values, plus Min and Max. Traced values also combine with plain decimal values on either side (traced * 1.1m). Bringing a calculation under Yurai means naming its inputs and taking the result back out at the boundary — the arithmetic expression itself does not have to be rewritten into a DSL. Mixed arithmetic uses explicit overloads; an implicit numeric conversion does not opt a plain value into tracing at an invisible call site.
  • Rounding as a recorded decision. Round(digits, reason) keeps the stated reason next to the step that changed the number — usually the most contested step in a money calculation.
  • The branch actually taken. Traced.If records which alternative produced the value, under the name you gave that decision, so "which rule fired?" is answerable.
  • Immutable evidence, shared structure. Evidence never changes once computed, values are safe to share across threads, and reusing an intermediate result records its derivation once rather than duplicating it.

Three ways out of the evidence:

Explain() The human-readable derivation above — for a code review, a ticket, or a conversation with a domain expert
ToJson() The same evidence as versioned JSON, for systems outside the process. It is material for an audit trail kept by your systems, not an audit trail in itself
DependsOn, Trace, Inputs The same evidence queried in code — assert in a test that a calculation still uses an input, or route a question by what a value depends on
var subtotal = (Traced.Of(100m, "BasePrice") - Traced.Of(10m, "Discount"))
    .As("Subtotal");
var total = (subtotal + 5m).As("Total");

bool stillUsesBasePrice = total.DependsOn("BasePrice");
IReadOnlyList<string> inputs = total.Inputs;
IReadOnlyList<IReadOnlyList<string>> paths = total.Trace("BasePrice");
// inputs: ["BasePrice", "Discount"]
// paths: [["BasePrice", "Subtotal", "Total"]]

Names use exact ordinal matching. DependsOn and Trace address both named inputs and results named with As; Inputs contains distinct input names only. Trace returns every matching path in deterministic order, projected from the matching name toward the result. Anonymous nodes remain part of dependency traversal but do not add a name to a path. DependsOn and Inputs are linear in graph size. Because Trace retains every path, its result can grow exponentially relative to the number of unique nodes in a heavily shared graph.

Two worked examples, with their expected output:

Working with traced values

A traced value is a different type from decimal, and that is a question worth answering directly: does it spread through the whole codebase?

It does not have to, and it should not. Trace the calculation you need to explain, and let plain values cross the boundary in both directions:

public decimal CalculateTotal(Order order)
{
    var basePrice = Traced.Of(order.BasePrice, "BasePrice");
    var total = (basePrice * (1 - Traced.Of(order.Discount, "MemberDiscount")))
        .Round(0, "Round to whole currency unit")
        .As("Total");

    _evidenceStore.Save(order.Id, total.ToJson());

    return total.Value;
}

Isolating the calculation that has to be explained is a design decision, not a workaround: the traced region stays small enough to read, and everything around it keeps its own types.

For help deciding where that boundary belongs, see Which calculations should use Yurai?.

Yurai is designed for explicitly bounded regions with tens of domain calculation steps, not for tracing every variable in an application. The published performance baseline includes graphs beyond 10,000 evidence nodes as stress cases and explains the time, allocation, and dependency-path scaling trade-offs.

Boolean conditions are another explicit boundary in the current v1 design. Reading .Value and evaluating a comparison produces a plain bool; if that boolean is passed to Traced.If, Yurai cannot recover which traced input decided it. An input used only by the condition therefore does not appear in v1 DependsOn or Trace. Q13 in #18 fixes this as a documented boundary: dependency queries cover recorded value derivation, not condition-only control dependency. A future traced-predicate API would be a separate capability.

Several projects answer nearby questions. The differences in scope and model are worth stating up front.

Project What it does How Yurai differs
Petit Poucet Java library for fine-grained explainability: compose functions from primitives and ask which parts of the input produced a given part of the output (CAV 2021) .NET rather than Java, and narrower on purpose — arithmetic on domain values written as ordinary C#, rather than a general lineage relation over composed functions and arbitrary data. No .NET port or fork of Petit Poucet was found as of this writing
Fluent.Calculations.Primitives .NET library (last released March 2024) for traceable business calculations: a Number/Condition value carries both an eagerly evaluated primitive and a captured expression via ordinary C# operators; its EvaluationScope adds named-calculation capture (readable source text) and cross-reference caching, with JSON export and optional DOT/Graphviz rendering GPL-3.0-only, versus Yurai's MIT; targets .NET 8.0 only, versus Yurai's netstandard2.0 reach back to .NET Framework 4.6.1; EvaluationScope is the library's higher-level capture/cache pattern, not a requirement for the underlying Number arithmetic — the sharper difference is that its structure is walked with a visitor a caller implements, versus Yurai's built-in DependsOn/Trace/Inputs queries
compprov-core Java framework (Apache-2.0) that wraps BigDecimal/BigInteger operations into a tracked Calculation Provenance Graph — a DAG of variables and data-flow edges — with snapshot, replay, diff, and input-substitution support Different ecosystem: Java has no operator overloading, so computations go through explicit wrapper calls and an env/context object rather than + - * /. It also depends on Jackson for serialization and ships snapshot/replay/diff/input-substitution features Yurai deliberately leaves out of scope. Listed as prior art for the same problem shape, not a .NET alternative
Appify.HumanReadableCalculationSteps Small NuGet package (Unlicense, .NET 8.0) where captioned decimal values combined with ordinary C# operators produce a linear, human-readable calculation-steps trace, aimed at invoices and payroll Produces formatted text, not a structure the program can query — no dependency-path API and no JSON export documented. Yurai's evidence is an immutable, structurally shared DAG that is queryable in code and exportable as JSON, not only printable
handcalcs Python library that renders calculation code as LaTeX with symbolic substitution, for notebooks and printed reports Produces a structure the running program can query and export, not a typeset document. Yurai renders no LaTeX and no HTML
Calcpad Free program for mathematical and engineering worksheets, with its own scripting language and HTML report output A library you call from an existing C# domain model, not a separate environment to author calculations in
NCalc, NTDLS.ExpressionParser, MathParser.org-mXparser .NET expression evaluators: they parse and evaluate expressions supplied as strings (mXparser also does symbolic calculus) at runtime Calculations stay compiled C# — type-checked, refactorable, reviewable. Yurai never parses an expression string, and it answers why a value is what it is rather than what a string expression evaluates to
Audit.NET .NET framework for audit trails: records operations and data changes through pluggable data providers Audit logging records what happened — which operation, when, by whom. Yurai answers why this value — the derivation inside a single calculation. Yurai also stores nothing; it returns the evidence and stops there

These comparisons are intentionally about each project's scope, ecosystem, license, runtime, and query model. They are limited observations about the entries listed here, not a uniqueness claim about Yurai. Yurai's claim is only about itself:

Yurai combines ordinary C# arithmetic over eagerly evaluated domain values with immutable, structurally shared derivation evidence and first-class dependency-path queries, in a zero-runtime-dependency netstandard2.0 library.

Non-goals

These are decisions, not gaps waiting to be filled.

Permanently out of scope:

  • Symbolic algebra — no expression rewriting, simplification, or solving. Yurai records how a concrete value was actually computed.
  • Sensitivity analysis and automatic differentiation — no derivatives, no what-if deltas.
  • Attribution — no apportionment of a result among its inputs, under any name.
  • Taint tracking and general-purpose data provenance — the evidence covers exactly the computation you chose to trace, and carries no security guarantee.
  • Audit-platform features — no storage, signing, timestamping, or retention. Yurai produces the material; persistence and integrity belong to your systems.
  • Evaluating expressions supplied as strings — calculations are compiled code.

Not planned, without being a redefinition of the library:

  • LaTeX or HTML rendering — text and JSON are the two exits. The JSON export exists so that richer rendering can be built outside the library.

Out of v1.0, with the future left open:

  • Value types other than decimal — the v1.0 public surface is decimal only, because that is the correct type for money and rate arithmetic. Yurai is a computation-lineage library whose first shipped value type is decimal, not a decimal library; extending it is an open question rather than a promise.

Installation

Install Yurai from NuGet:

dotnet add package Yurai

Quick start

The shortest path from a new console application to the first explanation is:

  1. Create a console application and add Yurai:

    dotnet new console -n YuraiQuickstart
    cd YuraiQuickstart
    dotnet add package Yurai --version 0.1.0
    
  2. Replace Program.cs with this complete example:

    using System;
    using Yurai;
    
    var basePrice = Traced.Of(1000m, "BasePrice");
    var discount = Traced.Of(0.10m, "MemberDiscount");
    var taxRate = Traced.Of(0.10m, "TaxRate");
    var total = (basePrice * (1 - discount) * (1 + taxRate))
        .Round(0, "Round to whole currency unit")
        .As("Total");
    
    Console.WriteLine(total.Explain());
    
  3. Run it:

    dotnet run
    

The first output is the Result / Derivation explanation shown at the top of this page.

Yurai targets netstandard2.0 and has zero runtime dependencies — BCL only, including the JSON export. NuGet compatibility therefore includes .NET Framework 4.6.1+ and .NET Core 2.0+, and every .NET release since, without pulling anything in behind it. On .NET Framework, Microsoft recommends 4.7.2 or later when consuming .NET Standard 2.0 libraries.

Project

License

MIT.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 101 8/11/2026