This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Documentation

Welcome to the Morphir .NET documentation!

This section contains all the information you need to understand and successfully use Morphir .NET.

Getting Started

New to Morphir .NET? Start here to learn the basics and get your environment set up.

Guides

Explore comprehensive guides covering various aspects of Morphir .NET development.

Specification

Detailed specifications for the Morphir IR format and JSON schemas.

API Reference

Complete API documentation for all Morphir .NET libraries and components.

1 - C# SDK

Immutable C# runtime for the bounded Morphir .NET SDK contract

Morphir.SDK.CSharp is an independent net10.0 runtime package. It uses the BCL and System.Collections.Immutable, with no FSharp.Core, Morphir compiler, model, CLI or Roslyn dependency. Existing Morphir.SDK consumers remain unchanged.

Install the package version published for your application, then use the Morphir.SDK.CSharp namespace. Both C#14 and C#15 consumers use the same API. The SDK does not require C#15 union syntax or a source generator.

using Morphir.SDK.CSharp;

var optional = Maybe<int>.Just(7).Map(x => Basics.Add(x, 1));
var result = Result<string, int>.Ok(7)
    .AndThen(x => x > 0
        ? Result<string, int>.Ok(x)
        : Result<string, int>.Err("negative"));
var old = Dict<string, int>.Singleton("a", 1, MorphirComparers.String);
var updated = old.Insert("b", 2);
// old still contains only a; updated enumerates a then b.

Use the API guide for constructors, immutable collections and error behavior. The compatibility guide describes the bounded contract, compiler profiles and runtime publication checks.

1.1 - C# SDK API

All public runtime types live in Morphir.SDK.CSharp. The frozen inventory uses logical Morphir names such as List.map; C# uses PascalCase instance methods and generic factories. Its parameter/result meanings are unchanged.

Morphir vocabularyPublic C# API
Maybe.Just, NothingMaybe<T>.Just(value), Maybe<T>.Nothing
Maybe.map, andThenvalue.Map(callback), value.AndThen(callback)
Result.Ok, ErrResult<TError,TValue>.Ok(value), .Err(error)
Result.map, andThenresult.Map(callback), result.AndThen(callback)
Unit; Basics.LT, EQ, GTUnit.Value; Order.LT, .EQ, .GT, also Basics constants
List literal, singleton, consList<T>.FromEnumerable(values), .Singleton(value), list.Cons(head)
List.map, append, head, tail, lengthlist.Map(f), .Append(right), .Head(), .Tail(), .Length
Dict.empty, singleton, fromListDict<K,V>.Empty(comparer), .Singleton(key,value,comparer), .FromEnumerable(pairs,comparer)
Dict.get, insert, update, remove, toList, uniondict.Get(key), .Insert(key,value), .Update(key,f), .Remove(key), .ToList(), .Union(right)
Set.empty, singleton, fromListSet<T>.Empty(comparer), .Singleton(value,comparer), .FromEnumerable(values,comparer)
Set.insert, remove, toList, unionset.Insert(value), .Remove(value), .ToList(), .Union(right)
Basics scalar operationsBasics.Add, Subtract, Multiply, Divide, IntegerDivide, ModBy, RemainderBy, Negate, Abs, Power, ToFloat, Round, Floor, Ceiling, Truncate, IsNaN, IsInfinite, Compare, LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual, Min, Max, Clamp, Equal, NotEqual
Decimal operationsDecimal.Add, Sub, Mul, Div, Compare, ToString

Use an alias such as using SdkDecimal = Morphir.SDK.CSharp.Decimal; when System.Decimal is also imported. SDK List<T> may similarly need an alias when System.Collections.Generic is imported outside the SDK namespace.

Maybe<T> is a readonly struct whose default is Nothing. Result<E,V> is a sealed immutable class with private construction, error type first and success type second. Neither uses null to encode a branch. Match observes either variant without throwing for absence. Constructors reject null payloads. Nothing and Err bypass map/bind callbacks. Called callback exceptions propagate without translation to Nothing or Err.

var text = Result<string, int>.Err("negative")
    .Match(error => error, value => value.ToString());
var absent = Maybe<int>.Nothing.Match(value => value, () => 0);

Collection factories copy enumerable inputs into immutable storage. Lists use ImmutableList, dictionaries ImmutableSortedDictionary, and sets ImmutableSortedSet. Public observations expose read-only enumeration and no builder, mutable array or writable backing collection. All updates return new immutable values. Dictionary construction retains the last duplicate value; union retains the left dictionary’s overlapping value. Update receives Just current or Nothing and either inserts Just value or removes Nothing.

using SdkList = Morphir.SDK.CSharp.List<int>;

var original = SdkList.FromEnumerable(new[] { 1, 2 });
var mapped = original.Map(x => Basics.Add(x, 1));
var keys = MorphirComparers.List(MorphirComparers.Int);
var dict = Dict<SdkList, Maybe<SdkList>>.Singleton(
    original, Maybe<SdkList>.Just(mapped), keys);
var equal = Dict<SdkList, Maybe<SdkList>>.Singleton(
    SdkList.FromEnumerable(new[] { 1, 2 }),
    Maybe<SdkList>.Just(SdkList.FromEnumerable(new[] { 2, 3 })), keys);
// dict.Equals(equal) and equal in-process hash codes.

Choose explicit MorphirComparers.Int, .Float, .Char, .String, .List(elementComparer) or .Tuple(...) for 2/3-tuples. Value validates boxed comparable values recursively and supports adapter-style heterogeneous object storage; individual list/key types must still respect the frozen homogeneous domain. There is no culture-sensitive CLR fallback. Bool, Decimal, Unit, Order, Maybe, Result, Dict, Set and callbacks have no admitted ordering. Float ordered keys must be finite. String ordering is ordinal UTF-16, without normalization.

SDK values implement structural Equals and consistent GetHashCode, including nested variants, collections and tuples. Decimal scale and signed floating zero do not distinguish equal hashes. NaN compares unequal, including inside SDK variants. Hash numbers are not a serialized format or cross-process contract. Generated records and variants opt in through the small IMorphirValue protocol. Implement MorphirEquals by checking the exact case/type and comparing every field with Basics.Equal. Implement MorphirHashCode with MorphirValue.Hash over the same fields. For C# record/native equality, route the typed Equals and GetHashCode overrides to those methods as well. This avoids CLR NaN equality and works after boxing or inside tuples, variants and collections.

sealed record Amount(double Number, Morphir.SDK.CSharp.List<int> Values)
    : IMorphirValue
{
    public bool MorphirEquals(IMorphirValue other) => other is Amount x
        && Basics.Equal(Number, x.Number) && Basics.Equal(Values, x.Values);
    public int MorphirHashCode() => MorphirValue.Hash((Number, Values));
    public bool Equals(Amount? other) => other is not null && MorphirEquals(other);
    public override int GetHashCode() => MorphirHashCode();
}

The protocol is an explicit caller contract. An interface cannot enforce physical immutability or correct custom equality. Generated code and application authors must keep payloads immutable and implement all fields consistently. Ordinary IEquatable objects, arrays, object identity and delegates do not automatically become Morphir values. MorphirValue.Hash is a .NET composition helper, not an extension to the frozen logical operation inventory.

Integers are checked Int32. Overflow raises OverflowException; integer zero division raises DivideByZeroException. RemainderBy(divisor, dividend) uses the dividend sign; ModBy uses the divisor sign. minInt % -1 is zero. Float is IEEE binary64. Half rounding resolves toward positive infinity. Float-to-Int conversion raises NonFiniteConversionException for NaN/infinity and OverflowException for an out-of-range rounded value. Clamp tests low first, then high, and preserves the selected value, including floating zero signs.

Decimal operates on the exact Decimal96 subset. Results remove redundant scale; Decimal.ToString uses invariant positional notation without trailing zeros. Decimal.Div(a,0) returns Nothing. Exact overflow raises OverflowException. Nonterminating fractions, results requiring scale above 28, negative Int powers, and unsupported ordering/equality domains raise NotSupportedException. They do not silently round, wrap, clamp or acquire CLR reference semantics.

1.2 - C# SDK compatibility

The bounded contract is morphir-dotnet-sdk-v1, schema 1, profile dotnet-sdk-v1: 67 operations, 207 required observations and eight separately reported representation exclusions. It does not claim the entire Morphir SDK. The checked-in fixtures retain independent upstream source evidence. Runtime test expectations are read from those fixtures, not computed by this runtime.

The excluded observations concern Int range or negative Int powers, supplementary Char, and exact Decimal96 representation/nontermination limits. The precise IDs and reasons remain in tests/Fixtures/MorphirSDK/cases.json. Strings support well-formed UTF-16 including supplementary scalars. Char accepts one non-surrogate BMP scalar. Dates, parsing, transcendental operations and unlisted collection functions are outside this inventory.

The runtime targets net10.0 and works with trimming and NativeAOT without reflection or dynamic code discovery. Runtime targeting is separate from the compiler SDK and numeric language version used by a consumer. Root global.json is not changed for isolated compiler verification. Ordinary package consumption must restore only Morphir.SDK.CSharp, without a project reference or repository source files. This is separate from the deferred Roslyn generator adapter.

The same ordinary consumer source was restored from an isolated local feed, compiled and executed under both exact toolchains below. Each fresh package cache contained only Morphir.SDK.CSharp; neither consumer referenced repository projects or source files.

Numeric language versionSDK/channelExact C# compiler build
14.010.0.403, stable5.9.0-1.26523.112, revision 5a2162bea6845eabc1fc721624a43568c65db3b1
15.011.0.100-rc.2.26504.105, RC25.12.0-1.26504.105, revision 820230c386db5b0106c5430b3e5aee92a7826bf5

Both target net10.0 and execute with runtime 10.0.14. SDK selection is disabled from rolling forward in each isolated global.json. The RC toolchain uses its matching 10.0.13 reference packs for net10.0 compilation; these tooling inputs are separate from package dependencies. The RC ZIP was checked against Microsoft’s release metadata. No root global.json, user setting or machine setting changed.

The same public consumer was also published and executed as self-contained trimmed and NativeAOT applications on Windows arm64, RID win-arm64, using SDK 10.0.403 and runtime 10.0.14. Both passed the ordinary consumer’s assertions and produced the same output. Trim and AOT warnings were treated as errors. The NativeAOT executable is an ARM64 native PE without a CLR header or managed Consumer.dll/coreclr.dll payload. This verifies that representative runtime consumer on that OS/RID, not every deployment target or every generic closure.

Reproduce with the checked-in PowerShell harness and installed exact toolchains:

& tests/Morphir.SDK.CSharp.Tests/verify-package-consumers.ps1 `
  -EvidenceDirectory C:/Temp/owned-fresh-sdk-proof `
  -StableDotnet C:/path/to/stable/dotnet.exe `
  -RcDotnet C:/path/to/isolated-rc/dotnet.exe

The RC directory needs its matching net10.0 targeting packs. NativeAOT needs Microsoft’s native prerequisites. The harness uses fresh local feeds/caches and process-local environment values, runs commands serially with bounded parallelism, and preserves logs on failure. SDK/compiler versions and numeric language versions are separate settings. This proof does not require native C# unions or implement the deferred source generator.

2 - Getting Started

Install the Morphir toolchain, compile your first model and generate code from it

Start here

Your first model with morphirup takes you from nothing to generated code: install morphirup, set up the toolchain, compile a small F# model and generate F# from it. It works on Windows and on Macs with Apple silicon, and needs no .NET.

Installation lists every way to install morphirup and the Morphir CLI.

Next

When something goes wrong, see Troubleshooting.

2.1 - Your first model with morphirup

Install morphirup, set up the toolchain, compile a small F# model, generate F# from it, then change a rule and see the result

In this walkthrough you install morphirup, set up the Morphir toolchain, compile a small F# model, and generate F# from it. Then you change one rule and see the change in the output. You don’t need .NET installed.

PlatformSupported
Windows x64 and ARM64Yes
macOS on Apple siliconYes
Linux x64 and ARM64morphirup installs, but setup doesn’t support Linux yet

Intel Macs are not supported. If something goes wrong, see Troubleshooting.

What morphirup is

morphirup installs the Morphir command line, morphir, together with the F# support it needs: a frontend that turns F# into Morphir IR, and a backend that turns Morphir IR into F#. morphirup run runs them for you.

1. Install morphirup

In PowerShell:

irm https://raw.githubusercontent.com/finos/morphir-dotnet/main/scripts/install-morphirup.ps1 | iex

In a terminal:

curl -fsSL https://raw.githubusercontent.com/finos/morphir-dotnet/main/scripts/install-morphirup.sh | sh

The script downloads the newest morphirup, checks it against the release’s checksums, and installs it:

morphirup-install: finding the newest release with morphirup-win-arm64.exe
morphirup-install: downloading morphirup-win-arm64.exe 0.4.0-alpha.5
morphirup-install: SHA-256 verified: 6bb59736f765a9f2cbdf7262a1f07838c15c6025927f10f7642f4307a8d237f3
morphirup-install: installed ...\morphirup.exe
morphirup-install: morphirup --version: 0.4.0-alpha.5+3abb0de0a9de5a321e109db5f2eef3afcdd429ea

Your version may be newer. Open a new terminal, then check that morphirup is found:

morphirup --version
0.4.0-alpha.5+3abb0de0a9de5a321e109db5f2eef3afcdd429ea

If the terminal says morphirup is not found, see morphirup is not found. Installation says where the script puts morphirup, lists its options, and has the other ways to install it.

2. Get the example project

The example has two files. Download them into a new folder, and run every later command in that folder:

New-Item -ItemType Directory -Force morphirup-fsharp\src | Out-Null
Set-Location morphirup-fsharp
$raw = 'https://raw.githubusercontent.com/finos/morphir-dotnet/main/examples/morphirup-fsharp'
Invoke-WebRequest "$raw/morphir.toml" -OutFile morphir.toml
Invoke-WebRequest "$raw/src/Pricing.fs" -OutFile src\Pricing.fs
mkdir -p morphirup-fsharp/src && cd morphirup-fsharp
raw=https://raw.githubusercontent.com/finos/morphir-dotnet/main/examples/morphirup-fsharp
curl -fsSL -o morphir.toml "$raw/morphir.toml"
curl -fsSL -o src/Pricing.fs "$raw/src/Pricing.fs"

If you have cloned the repository, the same files are in examples/morphirup-fsharp.

3. Set up the toolchain

See what setup would install, without installing anything:

morphirup setup --dry-run
morphirup: release-checksum delivery, NOT publisher-authenticated.
{
  ...
  "command": "setup",
  "status": "planned",
  ...
}
Run morphirup setup --yes to install and select this toolchain.

Then install it:

morphirup setup --yes
{
  ...
  "command": "setup",
  "status": "installed",
  ...
}
morphirup: release-checksum delivery, NOT publisher-authenticated.

Setup downloads the Morphir CLI, the F# frontend and the F# backend from the same release as morphirup. It checks every file against the release’s checksums, tries the tools out, and then makes them the ones morphirup run uses. It took about 35 seconds here. This is the only step that needs the network.

setup, show and run all print the line release-checksum delivery, NOT publisher-authenticated. It means the files matched the checksums the release published; it doesn’t mean they were signed. Troubleshooting says more.

Check what is installed:

morphirup show
{
  ...
  "command": "show",
  "status": "shown",
  ...
}
morphirup: release-checksum delivery, NOT publisher-authenticated.

"status": "shown" means a toolchain is set up and selected. Each setup --yes installs a new copy and selects it; morphirup select switches back to an earlier one. See the morphirup reference for both.

4. Read the model

morphir.toml names the project:

[project]
name = "acme/demo"
source_directory = "src"

[frontend]
language = "fsharp"

[ir]
format_version = 4

The package is acme/demo, its source is F# in the src folder, and Morphir IR is written in format 4.

src/Pricing.fs is the model:

namespace Acme.Demo
module Pricing =
    type Car = { Plate: string; Seats: int }
    let seats (car: Car) : int = car.Seats

A model is ordinary F#. This one has a type and a rule. A Car has a number plate and a number of seats. The rule seats says how many seats a car has to sell.

5. Compile the model

morphirup run -- compile
morphirup: release-checksum delivery, NOT publisher-authenticated.
compile succeeded: 1 module(s), IR version 4, ...\morphirup-fsharp\.morphir\out\compile.dest\morphir-ir.json; no diagnostics
...\morphirup-fsharp\.morphir\out\compile.dest\morphir-ir.json

Everything after -- goes to the Morphir CLI. It read morphir.toml, compiled src/Pricing.fs, and wrote the model as Morphir IR to .morphir/out/compile.dest/morphir-ir.json.

Morphir IR is your model as data that any Morphir tool can read. Open the file and you’ll find, among the details:

{
  "formatVersion": 4,
  "distribution": {
    "Library": {
      "packageName": "acme/demo",
      ...
        "modules": {
          "pricing": {
            ...
              "types": {
                "car": {
                  ...
                          "fields": {
                            "plate": {
                              ...
                                "fqname": "morphir/SDK:string#string"
                            ...
                            "seats": {
                              ...
                                "fqname": "morphir/SDK:basics#int"
              ...
              "values": {
                "seats": {
                  ...
                      "body": {
                        "Field": {
                          ...
                          "name": "seats"
  ...
}

The package acme/demo has one module, pricing. It has the type car, a record with a string plate and an int seats, and the rule seats, whose body reads the field seats. Morphir writes names without the F# capitals.

6. Generate F#

morphirup run -- generate --target fsharp --output generated
Get-Content generated\Pricing.fs
morphirup run -- generate --target fsharp --output generated
cat generated/Pricing.fs
morphirup: release-checksum delivery, NOT publisher-authenticated.
Pricing.fs
module Acme.Demo.Pricing

type Car = { plate: string; seats: int }

let seats (car: Car) : int = (car).seats

generate read the IR from the last compile and wrote generated/Pricing.fs. This F# comes from the IR, not from your source file, which is why the field names are in lower case.

7. Change a rule

Say the driver’s seat is not for sale. Keep a copy of the current output, so you can compare:

Copy-Item generated generated-before -Recurse
cp -R generated generated-before

Open src/Pricing.fs in an editor and change the last line:

    let seats (car: Car) : int = car.Seats - 1

Compile, generate and compare:

morphirup run -- compile
morphirup run -- generate --target fsharp --output generated
Compare-Object (Get-Content generated-before\Pricing.fs) (Get-Content generated\Pricing.fs)
morphirup run -- compile
morphirup run -- generate --target fsharp --output generated
diff generated-before/Pricing.fs generated/Pricing.fs

On Windows the comparison ends with:

InputObject                                          SideIndicator
-----------                                          -------------
let seats (car: Car) : int = ((-) ((car).seats) (1)) =>
let seats (car: Car) : int = (car).seats             <=

<= marks the old line and => the new one. The generated F# now subtracts 1 from the seats. It writes car.seats - 1 as a call of the operator (-), which means the same. That’s the whole loop: change the model, compile, generate.

8. Work offline

Setup needed the network. Compile and generate don’t. To see this, point the terminal at a proxy that doesn’t exist, so that every download fails:

$env:HTTPS_PROXY = 'http://127.0.0.1:9'
$env:HTTP_PROXY = 'http://127.0.0.1:9'
morphirup setup --dry-run
morphirup run -- compile
morphirup run -- generate --target fsharp --output generated
Remove-Item Env:HTTPS_PROXY, Env:HTTP_PROXY
export HTTPS_PROXY=http://127.0.0.1:9 HTTP_PROXY=http://127.0.0.1:9
morphirup setup --dry-run
morphirup run -- compile
morphirup run -- generate --target fsharp --output generated
unset HTTPS_PROXY HTTP_PROXY

setup --dry-run fails because it can’t reach the release:

  "status": "refused",
  ...
    "code": "acquisition.refused",
    "message": "The bounded release transfer was refused by transport or byte-integrity checks."

Compile and generate still succeed, with the same output as before.

Next steps

2.2 - Installation

Install morphirup, the Morphir toolchain manager, then the older routes to the morphir CLI

Start with morphirup, the Morphir toolchain manager. It is one file and needs nothing else installed. It sets up the Morphir toolchain for your project, which then compiles and generates. When it is installed, follow Your first model.

Install morphirup

On macOS (Apple silicon) and Linux (x64, ARM64):

curl -fsSL https://raw.githubusercontent.com/finos/morphir-dotnet/main/scripts/install-morphirup.sh | sh

On Windows (x64, ARM64), in PowerShell:

irm https://raw.githubusercontent.com/finos/morphir-dotnet/main/scripts/install-morphirup.ps1 | iex

The script installs the newest release for your machine and verifies the download first:

  • On macOS and Linux it installs ~/.local/bin/morphirup. If that folder is not on your PATH, the script prints the line to add to your shell profile.
  • On Windows it installs %LOCALAPPDATA%\Programs\morphirup\morphirup.exe and adds the folder to your user PATH.

Open a new terminal and check that it runs:

morphirup --version

It prints the version you installed.

Script options

TomacOS and LinuxWindows
Install a given release--version <version>-Version <version>
Install into another folder--install-dir <dir>-InstallDir <dir>
Leave PATH aloneThe script never changes it-NoModifyPath

The versions are on the releases page. To pass an option:

curl -fsSL https://raw.githubusercontent.com/finos/morphir-dotnet/main/scripts/install-morphirup.sh | sh -s -- --version <version>
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/finos/morphir-dotnet/main/scripts/install-morphirup.ps1))) -Version <version>

The environment variables MORPHIRUP_VERSION and MORPHIRUP_INSTALL_DIR work too, on every platform.

Behind a proxy or firewall

The scripts need to reach raw.githubusercontent.com (for the script itself), github.com and release-assets.githubusercontent.com. Without --version, they also ask api.github.com for the newest release, and read the release list on github.com if that host is blocked.

  • Proxy. On macOS and Linux, set HTTPS_PROXY (and NO_PROXY for internal hosts), for example export HTTPS_PROXY=http://proxy.example.com:8080. On Windows, the script uses the Windows proxy settings, or HTTPS_PROXY when you set it, and signs in to the proxy as you.
  • TLS inspection. On macOS and Linux, point CURL_CA_BUNDLE (or SSL_CERT_FILE for wget) at your company’s CA bundle. On Windows, the company’s root certificate must be in the Windows certificate store, which IT usually manages.
  • Mirror. If your company mirrors GitHub release downloads, set MORPHIRUP_DOWNLOAD_URL to the mirror of https://github.com/finos/morphir-dotnet/releases/download. The mirror must serve HTTPS and keep the same layout, <url>/v<version>/<file>. Pass --version / -Version if the mirror is all you can reach.

With mise

mise use -g 'github:finos/morphir-dotnet[matching=morphirup,version_prefix=v,prerelease=true]'
morphirup --version

To install a given release, add @<version> at the end, inside the quotes.

Download it yourself

From the releases page, download the file for your machine and the release’s checksum file morphirup-v<version>.sha256:

MachineFile
macOS, Apple siliconmorphirup-osx-arm64
Linux x64morphirup-linux-x64
Linux ARM64morphirup-linux-arm64
Windows x64morphirup-win-x64.exe
Windows ARM64morphirup-win-arm64.exe

Check the file before you run it. On macOS and Linux, in the download folder (on Linux, sha256sum -c - works too):

f=morphirup-osx-arm64; v='<version>'
grep "  $f\$" "morphirup-v$v.sha256" | shasum -a 256 -c -
chmod +x "$f" && mkdir -p ~/.local/bin && mv "$f" ~/.local/bin/morphirup

It prints morphirup-osx-arm64: OK. If it prints anything else, delete the file.

On Windows, in PowerShell:

$f = 'morphirup-win-x64.exe'; $v = '<version>'
$expected = (Select-String -Path "morphirup-v$v.sha256" -Pattern "  $f$").Line.Split(' ')[0]
(Get-FileHash $f -Algorithm SHA256).Hash -eq $expected

It prints True. Rename the file to morphirup.exe and move it into a folder on your PATH.

What works on each platform

PlatformInstall morphirupSet up, compile and generate
macOS on Apple siliconYesYes
Windows x64 and ARM64YesYes
Linux x64 and ARM64YesNot yet: morphirup setup does not support Linux yet
macOS on IntelNoNo

On Linux, morphirup needs glibc 2.35 or later (Ubuntu 22.04, Debian 12 or later). Alpine and other musl distributions are not supported. Until setup supports Linux, use the standalone morphir CLI below.

The morphirup reference covers the release files and how they are checked. If something goes wrong, see Troubleshooting.

Other ways to install the standalone morphir CLI

These routes install the morphir command line itself, without morphirup.

Requirements

What morphir-dotnet needs from the machine depends on the offering. A .NET runtime is what runs a managed program; a .NET SDK is the larger download that also builds and installs one. Each offering states its needs in morphir extension list --json (deployment and prerequisites, see extension list):

OfferingTo run itTo install itReleased
Native AOT morphirNothing from .NETNothingNot yet as this offering
Self-contained morphir (carries its own .NET runtime)Nothing from .NETNothingNot yet
Framework-dependent morphirThe .NET runtime Microsoft.NETCore.App 11.xNothingNot yet
dotnet-morphir (Morphir.Tool dotnet tool)The .NET runtime Microsoft.NETCore.App 11.xA .NET SDK 11.0.100 or later, for dotnet tool installYes, on NuGet

The morphir-<rid> executables that releases attach today are a trimmed single-file build that carries its runtime, so they need nothing from .NET either; no named release profile produces them, so they report no deployment. Building from source needs the .NET SDK that global.json names.

Global Tool Installation

Install the Morphir CLI as a global .NET tool. The command is dotnet-morphir:

dotnet tool install --global Morphir.Tool --prerelease

Standalone Executable

The standalone morphir is one file that carries its .NET runtime, so it needs no .NET SDK or runtime. It is a managed, trimmed, self-contained single-file build without an F# frontend, not a Native AOT executable. Download morphir-linux-x64, morphir-linux-arm64, morphir-osx-arm64, morphir-win-x64.exe or morphir-win-arm64.exe from the releases page. The Windows ARM64 file is built on a native Windows 11 ARM64 runner; releases before the first one that lists it have no win-arm64 file. To compile F# with it, also install the F# frontend extension:

dotnet tool install --global Morphir.Frontend.FSharp.Extension --prerelease

The extension tool needs the .NET runtime. On a machine without .NET, use the self-contained frontend folder instead. Releases do not publish it yet; see Compile without .NET installed for how to build it and what it supports: compiling supported F# source files to Morphir IR, not building .fsproj projects. The folder is not trimmed and is not a single file. For win-arm64 it is about 120 MB, and its zip archive about 47 MB. The exact file count and sizes of a build are in the log of the build target PublishFSharpFrontendProcess, which prints them for the folder and the archive.

PlatformSelf-contained F# frontend
Windows ARM64 (win-arm64)Checked on Windows 11 ARM64 with the machine’s .NET hidden from the frontend (no dotnet on PATH, no DOTNET_ROOT, no global install); not run on a machine with no .NET at all
Windows x64 (win-x64)Builds; ran under x64 emulation on Windows 11 ARM64 only, not qualified
Linux glibc x64 (linux-x64)Builds; not run, not qualified
Linux glibc ARM64, macOS x64, macOS ARM64Not qualified

The operating system must still meet the .NET 10 runtime requirements. On Linux these include glibc and ICU (libicu), which the folder does not contain; this is not verified yet.

Both hosts generate F# with their built-in backend by default. A separate F# backend extension, Morphir.CodeGeneration.FSharp.Extension (command morphir-fsharp-backend), is for MEP hosts that have no built-in F# backend. morphir generate runs it only when you select it, with --extension or [codegen.fsharp].extension. See F# generation.

Standalone morphir install scripts

These scripts install the standalone morphir executable, not morphirup:

# Linux
curl -fsSL https://raw.githubusercontent.com/finos/morphir-dotnet/main/scripts/install-linux.sh | bash

# macOS
curl -fsSL https://raw.githubusercontent.com/finos/morphir-dotnet/main/scripts/install-macos.sh | bash
# Windows
irm https://raw.githubusercontent.com/finos/morphir-dotnet/main/scripts/install-windows.ps1 | iex

See scripts/README.md for their options.

Local Project Installation

Add the Morphir libraries to your project:

dotnet add package Morphir.Core

Build from Source

git clone https://github.com/finos/morphir-dotnet.git
cd morphir-dotnet
dotnet build

Verify the morphir CLI

Check that the dotnet tool is installed correctly:

dotnet-morphir --version

Then follow Compile F# to Morphir IR.

Troubleshooting

See Troubleshooting.

Release archives (not published yet)

You cannot download these yet: releases do not publish them, and no release or platform is qualified for them. This section describes what the build packs, so that the troubleshooting below has something to refer to. Use the installation methods above.

The build can pack four offerings of the same release. Each archive holds its files at the root; extracted anywhere, including a path with spaces, it runs as morphir (morphir.exe on Windows).

OfferingArtifactNeeds on the machineF#
native-aotmorphir-dotnet-<release>-native-aot-<rid>.zip or .tar.gzNothing from .NETThrough the F# frontend extension (--extension)
self-containedmorphir-dotnet-<release>-self-contained-<rid>.zip or .tar.gzNothing from .NETBuilt in
framework-dependentmorphir-dotnet-<release>-framework-dependent-<rid>.zip or .tar.gzThe .NET 10 runtime (Microsoft.NETCore.App 10.0) for the same architectureBuilt in
dotnet-toolMorphir.Tool.<release>.nupkg, as this build packs itThe .NET SDK 10.0 to install it, and the .NET 10 runtime to run itBuilt in

The dotnet-tool row is the package that this build packs from the current source. It is not the Morphir.Tool package already on NuGet that the global tool installation installs.

The operating-system prerequisites of these archives are not established: the minimum glibc version on Linux, whether ICU and OpenSSL libraries must be installed, the minimum macOS version, and what Gatekeeper does with a downloaded, unsigned executable. Only “needs nothing from .NET” and “needs the .NET 10 runtime” have been checked, on one win-arm64 build machine.

When the framework-dependent morphir cannot find .NET

The framework-dependent morphir, like dotnet-morphir, cannot start without a .NET runtime, so it cannot explain the problem itself. The .NET host prints the message instead. There are two cases.

No .NET found. The host prints “You must install .NET to run this application.” This Windows example comes from the .NET 10.0.14 host on Windows ARM64, where it exited with 0x80008083 (-2147450749):

You must install .NET to run this application.

App: <folder>\morphir.exe
Architecture: arm64
App host version: 10.0.14
.NET location: Not found

The following locations were searched:
  Application directory:
    <folder>\
  Environment variable:
    DOTNET_ROOT_ARM64 = <not set>
    DOTNET_ROOT = <not set>
  Registered location:
    HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\arm64\InstallLocation = <not set>
  Default location:
    C:\Program Files\dotnet

Learn more:
https://aka.ms/dotnet/app-launch-failed

Download the .NET runtime:
https://aka.ms/dotnet-core-applaunch?missing_runtime=true&arch=arm64&rid=win-arm64&os=win10&apphost_version=10.0.14

Another .NET version found. More common is a machine with .NET, but not a version this morphir can run on. The host then prints “You must install or update .NET to run this application.”, the framework and version it needs, and the frameworks it found. This Windows example was made by asking for a version the machine does not have; it exited with 0x80008096 (-2147450730):

You must install or update .NET to run this application.

App: <folder>\morphir.exe
Architecture: arm64
Framework: 'Microsoft.NETCore.App', version '11.0.0' (arm64)
.NET location: <dotnet folder>

The following frameworks were found:
  10.0.13 at [<dotnet folder>\shared\Microsoft.NETCore.App]
  10.0.14 at [<dotnet folder>\shared\Microsoft.NETCore.App]

Learn more:
https://aka.ms/dotnet/app-launch-failed

To install missing framework, download:
https://aka.ms/dotnet-core-applaunch?framework=Microsoft.NETCore.App&framework_version=11.0.0&arch=arm64&rid=win-arm64&os=win10

The released framework-dependent morphir asks for Microsoft.NETCore.App 10.0.0 with the default roll-forward: any installed 10.0 patch serves (the newest one is used), and so does a later 10.x minor when no 10.0 patch is installed. .NET 9 or .NET 11 alone does not.

On Linux and macOS the messages name Unix paths, and the “Registered location” line names /etc/dotnet/install_location instead of the registry. A shell there sees only the low 8 bits of the exit code: 131 for “no .NET” and 150 for “another version”. These Linux and macOS details follow from the host’s documented behavior and are not verified here.

The host looks for .NET in this order: the application folder (where a self-contained application carries it), the DOTNET_ROOT_<ARCH> and DOTNET_ROOT variables, the registered location, and the default location.

Operating systemRegistered locationDefault location
WindowsHKLM\SOFTWARE\dotnet\Setup\InstalledVersions\<arch>\InstallLocationC:\Program Files\dotnet
Linux/etc/dotnet/install_location, or /etc/dotnet/install_location_<arch>/usr/share/dotnet, or /usr/lib/dotnet for some distribution packages
macOS/etc/dotnet/install_location, or /etc/dotnet/install_location_<arch>/usr/local/share/dotnet

An x64 morphir running under emulation on an ARM64 machine (Windows or macOS) looks for the x64 .NET in the x64 subfolder of the default location, for example C:\Program Files\dotnet\x64 or /usr/local/share/dotnet/x64, and reads DOTNET_ROOT_X64. Only the Windows row was observed here, in the host’s own output on Windows ARM64; the Linux and macOS rows and the emulation subfolder follow the host’s documented behavior and are not verified here.

To fix it, do one of these:

  1. Install the .NET 10 runtime for the architecture that the message names, from the download link it prints.
  2. If .NET is installed in a folder of your own, set DOTNET_ROOT (or DOTNET_ROOT_ARM64, DOTNET_ROOT_X64) to that folder.
  3. Use the self-contained or native-aot archive instead; neither needs an installed .NET.

2.3 - Compile F# to Morphir IR

Write a business rule in F#, compile it to Morphir IR, and generate code from it in five minutes

Acme Car Rental keeps its daily rates in F#. In five minutes you turn that rule into Morphir IR, and then into C# that another team can call.

graph LR
    A[Rates.fs] -->|dotnet-morphir compile| B[morphir-ir.json]
    B -->|dotnet-morphir generate| C[C# or F# code]

1. Install the tool

You need the .NET SDK 10.0 or later.

dotnet tool install --global Morphir.Tool --prerelease
dotnet-morphir --version

2. Write the rule

mkdir -p acme/src && cd acme

Save this as src/Rates.fs:

namespace Acme.CarRental

module Rates =
    type VehicleClass =
        | Economy
        | Compact
        | Suv

    let dailyRate (vehicle: VehicleClass) : int =
        match vehicle with
        | Economy -> 4000
        | Compact -> 5000
        | Suv -> 8000

3. Compile it

dotnet-morphir compile --language fsharp --input src/Rates.fs --package-name acme/car-rental
compile succeeded: 1 module(s), IR version 4, /work/acme/.morphir/out/compile.dest/morphir-ir.json; no diagnostics
/work/acme/.morphir/out/compile.dest/morphir-ir.json

The IR is in .morphir/out/compile.dest/morphir-ir.json. Every later command can find it there.

4. Generate code

dotnet-morphir generate --target csharp --output generated/csharp
p_Acme.CarRental/m_RatesModule.g.cs

generate reads the IR of the most recent compile, so it needs no --input. Use --target fsharp for F#.

Do both steps in one run:

dotnet-morphir compile --language fsharp --input src/Rates.fs --package-name acme/car-rental \
  --target csharp --generate-output generated/csharp

Compile without .NET installed

To compile F# on a machine with no .NET, use morphirup: it installs the Morphir CLI with its own .NET runtime and the F# frontend. Follow Your first model with morphirup. To build the self-contained F# frontend from a checkout instead, see Build the self-contained F# frontend.

Next steps

2.4 - Validating IR Files

Check a Morphir IR file with morphir validate

morphir validate reads a Morphir IR file and checks it. Use it on IR that compile wrote, or on IR from another tool. The commands below run the Morphir CLI through morphirup, as in Your first model with morphirup. With the .NET tool, write dotnet-morphir in place of morphirup run --.

Check a file

In the walkthrough’s project, after morphirup run -- compile:

morphirup run -- validate --input .morphir/out/compile.dest/morphir-ir.json
0 errors, 0 warnings, 0 info, 0 hints. Analyzers: 1 ran, 0 not run, 0 failed.

The exit code is 0 when there are no errors, so you can use the command in a script or a CI job. The checks find type mismatches, such as a string where the model declares an int.

For a report that a script can read, add --json:

morphirup run -- validate --input .morphir/out/compile.dest/morphir-ir.json --json
{
  "host": "morphir",
  "success": true,
  "exitCode": 0,
  ...
  "analyzers": [
    {
      "id": "morphir.type-validator",
      "status": "ran"
    }
  ],
  "diagnostics": []
}

When the file can’t be read

If the file is not IR that Morphir can read, validate stops with exit code 2 and says where the problem is. Here formatVersion is the string "3" instead of the number 3:

Error: unsupported-version: ...\bad-ir.json at $.formatVersion: Expected a classic IR version: integer 1, 2 or 3, or release 3.0.x or 3.1.x.

Troubleshooting lists the usual causes.

Check against the JSON schema

morphir ir verify <file> checks a file against the JSON schema of IR formats 1, 2 and 3. Today it also reports errors for IR that compile writes, in format 4 and in format 3, so use validate for those files. See the ir verify reference.

Next steps

2.5 - Generate F# from Morphir

Build and run a car rental model using the Myriad plugin

Compile a Morphir model JSON file into F# as part of an ordinary build. This article uses the supplied car rental example and calls its generated inventory and billing functions.

Prerequisites

  • A checkout of finos/morphir-dotnet.
  • The .NET SDK pinned in global.json, currently 10.0.403.
  • Access to NuGet for package restore.

Run the following commands from the repository root. The checked-in model is sufficient; a separate Morphir frontend is not required.

Choose the plugin version

Morphir.Myriad is on NuGet.org, with a version for each morphir-dotnet release. This article uses 0.4.0-alpha.2; use the version of the release you want. The package brings everything the build needs; the plugin reference lists the versions it works with.

Configure the model input

The example’s examples/car-rental/CarRental.fsproj references the plugin and declares the JSON input once:

<ItemGroup>
  <PackageReference Include="Morphir.Myriad" Version="0.4.0-alpha.2" PrivateAssets="all" />
  <MorphirModel Include="models/car-rental-v3.json" />
  <Compile Include="Program.fs" />
</ItemGroup>

In your own project, write the version as above. In this repository the example takes it from the property MorphirMyriadVersion, which is why the commands below pass -p:MorphirMyriadVersion. Don’t add the JSON file to Compile, and don’t list the generated modules by hand.

Restore and run

dotnet restore examples/car-rental/CarRental.fsproj \
  --configfile examples/car-rental/NuGet.config -p:MorphirMyriadVersion=0.4.0-alpha.2
dotnet run --project examples/car-rental/CarRental.fsproj --no-restore -p:MorphirMyriadVersion=0.4.0-alpha.2

The supplied NuGet configuration restores every package from NuGet.org, and keeps the repository’s own source mapping out of the example. The application prints:

dailyRate=5000
inventory=Available
reservation=Reserved
pickup=Rented
return=Available
invoice=15000
rejections=7

The first generated value is Car.Rental.Inventory.dailyRate. The driver also calls Inventory.lookup "CAR-1" [ Inventory.car ], then executes the reservation, pickup, return and billing functions. Three billable days at 5000 cents per day produce a 15000-cent invoice. The final line confirms that seven invalid operations returned the expected business errors.

Inspect the F# output

Look beneath examples/car-rental/obj/<configuration>/net10.0/morphir/. The model has seven generated files: Domain.fs, Inventory.fs, Reservation.fs, Pickup.fs, Return.fs, Billing.fs and Workflow.fs. The accompanying manifest.json lists them in dependency order. Myriad inserts them before Program.fs in compilation.

Edit the model JSON or its optional F# authoring script, then rebuild. Generated files are build artifacts and are regenerated; do not edit them directly.

Next steps

Follow the car rental tutorial for the full model, state transitions, error cases and a rate-change exercise. Consult the plugin reference for configuration, compatibility and build failures.

2.6 - Configure a workspace

Workspace discovery, inspection, safe TOML editing and .NET consumption

Configure a workspace

morphir.toml declares a workspace, its member projects and their settings. This page shows how to declare a workspace, inspect the settings that apply, add a personal override, and move a morphir-elm project to morphir.toml.

Declare the workspace

Create morphir.toml at the repository root:

[workspace]
members = ["packages/*"]
exclude = ["packages/experimental-*"]
default_member = "packages/orders"
out_dir = ".morphir/out"

[ir]
format_version = 4

[ui]
theme = "light"

Create packages/orders/morphir.toml for the member project:

[project]
name = "Acme.Orders"
source_directory = "src"
exposed_modules = ["Orders.Api"]

Create its src folder. A command run in packages/orders/src finds both files: the member’s own, and the workspace’s at the root. The root has no [project] section, so it is only a workspace, not another project.

To discover a workspace from your own .NET code, see Discover a workspace from a host library.

Inspect the effective values

From the repository root, these commands inspect settings and discovery:

morphir config get ir.format_version --show-origin
morphir config path
morphir config explain ui.theme

The format version comes from the root TOML file. path reports consulted sources and their status; explain identifies every contributing layer for the theme and the winner.

For a member-only value, select its canonical workspace path:

morphir config get project.name --project packages/orders --json
morphir config get ui.theme --workspace --show-origin --show-scope
morphir config list --isolated --json

A scoped query does not fill absent settings from other scopes. --file path inspects only that file; --config path adds a selected-file layer during effective resolution. Empty strings and arrays succeed; a missing key exits 1. Usage or load failures exit 2. See the query API and Microsoft host example to use these results without the CLI. Existing IR commands do not adopt all settings merely because inspection can show them.

Add and remove a personal override

Preview and apply a personal override from the same directory:

morphir config set ui.theme dark --user --dry-run --json
morphir config set ui.theme dark --user
morphir config get ui.theme --show-origin
morphir config unset ui.theme --user
morphir config get ui.theme --show-origin

The workspace declares packages/orders as its default member, so --user targets packages/orders/morphir.user.toml. Without a selected member, a virtual workspace root uses root-adjacent morphir.user.toml. Without a higher override, the first query returns dark from that file. After unset, it returns inherited light from the workspace primary. Removal reveals the inherited value rather than deleting the setting from every source.

Add morphir.user.*, applicable member override paths, and .morphir/out/ to your ignore policy. The configuration system does not change ignore files automatically.

Preview reports the target, redacted before/after values and the effective winner without writing. If an environment variable or profile still wins, a successful file edit still exits 0. For example:

MORPHIR_UI__THEME=light morphir config set ui.theme dark --user --json
morphir config set ui.color false --type bool --workspace --dry-run

Running the same set again reports unchanged and doesn’t rewrite the file. unset of a missing value exits 1. An invalid value, a file you can’t write, or a file that changed while you edited it exits 2: look at the file and run the command again. The config reference has the details.

Retain an existing JSON project

A member may retain its morphir-elm morphir.json:

{
  "name": "Acme.Orders",
  "sourceDirectory": "elm-src",
  "exposedModules": ["Orders.Api"]
}

If a TOML primary also sets project.source_directory = "src", that value wins while the JSON-only fields survive. A scoped edit creates or updates TOML and leaves JSON unchanged. Removing that TOML key reveals elm-src again. A legacy field and its canonical spelling in the same JSON file are an error.

Migrate the classic project

The command is available through the CLI and shared migration APIs.

For this example, start with a classic project whose only primary is the JSON file above. If the earlier TOML example is also present, its different source directory is a conflict that migration must report before writing.

Preview the conversion, then apply it to that member:

morphir project migrate packages/orders/morphir.json --dry-run --json > migration-plan.json
morphir project migrate packages/orders/morphir.json
morphir project migrate packages/orders/morphir.json --json # unchanged repeat

The resulting packages/orders/morphir.toml includes the mapped fields and the defaults that previously came from recognizing a classic Elm manifest:

[project]
name = "Acme.Orders"
source_directory = "elm-src"
exposed_modules = ["Orders.Api"]

[frontend]
language = "elm"

[pipeline]
backend = "checkpoint"
checkpoint_format = "morphir-json"

Migration checks that effective project values and source-path meaning are unchanged when the classic source is excluded. The comparison excludes machine, personal, environment, CLI, and activated-profile overlays while retaining declared base workspace context. It does not pin a new IR version or copy those overrides into the project file. The result reports verification and any fields requiring manual decisions.

The source JSON stays unchanged so older morphir-elm consumers can continue using it. Repeating migration with the same inputs makes no changes. A conflicting TOML value, unsupported dependency/decorations shape, or any unmapped setting blocks application rather than silently dropping data. Remove the old manifest separately only after those consumers have moved to the new format.

See the migration command reference for output selection and structured dry-run results.

Adopt a repository of classic projects

If the repository has several classic projects and no new workspace yet, preview the bounded workspace migration from its root:

morphir workspace migrate . --include 'packages/*' --exclude 'packages/experimental-*' --dry-run
morphir workspace migrate . --include 'packages/*' --exclude 'packages/experimental-*'

The plan lists projects, exclusions, nested workspace boundaries, already migrated members, TOML edits, proposed membership, and intended member/default-selection and shared-output-root changes. It checks each classic-to-TOML conversion in the same proposed workspace context and verifies the resulting workspace before applying the batch. One blocked selected project blocks the whole plan. Existing membership/settings and every classic manifest are preserved; an equivalent repeat makes no changes.

Adopting a shared workspace can change standalone output defaults to <workspace>/.morphir/out. The preview reports that change separately from serialization equivalence; it does not move previously generated files.

For two eligible projects under packages/orders and packages/payments, a new virtual workspace contains explicit member paths:

[workspace]
members = ["packages/orders", "packages/payments"]

Each converted member receives its own TOML primary with the same loss checks as single-project migration. If the repository root itself has a project, migration keeps it as the root project rather than adding "." to the member list. Existing membership patterns, exclusions, valid default selection, comments, and settings remain in place. Preview the operation from the directory you intend to adopt; invoking from a child deliberately limits the operation to that child.

Use JSON in automation and retain the result when application fails:

morphir workspace migrate . --dry-run --json > workspace-preview.json
morphir workspace migrate . --json > workspace-result.json
morphir workspace migrate . --json # unchanged repeat

Exit code 2 means the migration was blocked or failed. If the result says recovery is required, don’t run it again yet: follow the recovery guide.

Read the workspace migration reference for batch recovery and supported controls.

Inspect configuration in CI

Create morphir.profile.ci.toml beside the root primary:

[ui]
color = false

The shared loader accepts explicitly selected profiles. This command selects the profile and captures JSON output:

morphir config show --isolated --discovery-boundary . --profiles ci --json > effective-config.json

This excludes machine configuration and personal overrides while retaining the selected profile. Environment values still participate. Stdout contains JSON; diagnostics go to stderr. Merely setting CI=true does not activate a named profile through the shared loader. Legacy cache callers retain their separate CiProfileMode behavior.

Read the configuration guide for all layouts and precedence rules, the command reference for options, and troubleshooting for unexpected results.

2.7 - Python WASM extension (experimental)

Compile and generate Python with the pinned Python extension in the .NET CLI

This experimental feature runs the Python 0.6.0 extension inside the Morphir CLI. It compiles a small Python file to Morphir IR and generates Python from it. You don’t need Python installed.

Check that your CLI can run WASM extensions with morphir extension runtimes --json. It can on Windows ARM64, macOS and Linux (glibc). Windows x64 can’t run WASM extensions yet (#609); the other commands still work there. From a new project directory, download and verify the published module with PowerShell:

New-Item -ItemType Directory -Force .morphir/extensions | Out-Null
Invoke-WebRequest 'https://github.com/finos/morphir-python/releases/download/extension/python/v0.6.0/morphir-python-binding-0.6.0.wasm' -OutFile .morphir/extensions/python.wasm
if ((Get-FileHash .morphir/extensions/python.wasm -Algorithm SHA256).Hash.ToLowerInvariant() -ne 'ce0cba07f0395ff8179b42eb021ade05c68197f69b8e9c0c8bc369d2a035727c') { throw 'Python module digest mismatch' }

Create morphir.toml:

[project]
name = "acme/demo"

[extensions.python]
path = ".morphir/extensions/python.wasm"
abi = "extism-kernel-v1"
sha256 = "ce0cba07f0395ff8179b42eb021ade05c68197f69b8e9c0c8bc369d2a035727c"

Create identity.py:

def identity(value: int) -> int:
    return value

Compile it, then generate Python from the successful compile record:

morphir compile --project . --language python --input identity.py --ir-version 4 --json
morphir generate --project . --target python --output generated --json

To generate from an existing IR file instead:

morphir generate --project . --input morphir-ir.json --target python --output generated --json

The [extensions.python] table makes --language python and --target python use the module. The CLI checks the module’s sha256 before it runs it. JSON results go to stdout and the extension’s messages to stderr. If the extension fails, generate writes no files. The WASM extension reference explains how the CLI picks an extension and the limits it applies.

The extension supports a small part of Python. Code it doesn’t support, including addition, gives a source diagnostic and exit code 1, and no IR.

2.8 - Troubleshooting

Fixes for the problems you are most likely to meet when you install morphirup, set up a toolchain, compile and generate

Each problem below gives what you see, why it happens, and what to do. For problems with a single command, see also the CLI troubleshooting reference.

Installing morphirup

morphirup is not found

What you see. After installing, the terminal says morphirup is not recognized, or command not found.

Why. The folder that holds morphirup is not on your PATH, or the terminal was open before the install.

What to do. On Windows, the install script adds its folder to your user PATH, so open a new terminal; one that was already open doesn’t see the change. On macOS and Linux, the script doesn’t change PATH. When its folder isn’t on your PATH, it ends by printing the line to add:

morphirup-install: .../bin is not on your PATH. Add this line to your shell profile
morphirup-install: (~/.profile, ~/.bashrc or ~/.zshrc), then open a new shell:

    export PATH=".../bin:$PATH"

The script prints the full folder where this shows .... Add its export line to your shell profile and open a new shell. Installation says where each script installs morphirup.

The install script reports a checksum mismatch

What you see.

SHA-256 mismatch for morphirup-win-arm64.exe: expected 0000000000000000000000000000000000000000000000000000000000000000, got 6bb59736f765a9f2cbdf7262a1f07838c15c6025927f10f7642f4307a8d237f3; nothing was installed

Why. The file the script downloaded is not the one the release published. Usually the download was cut short or changed on the way.

What to do. Nothing was installed, so it is safe to run the script again. If the mismatch comes back, download morphirup and the release’s checksum file yourself and compare them, as Download it yourself shows. If they still don’t match, don’t run the file, and open an issue.

The install script refuses on an Intel Mac

What you see. On an Intel Mac, the script stops with an error that says Intel macOS is not supported. (A terminal that runs under Rosetta on an Apple silicon Mac is fine: the script installs the Apple silicon file.)

Why. morphirup is built for Macs with Apple silicon (M1 and later) only.

What to do. Use a Mac with Apple silicon, or a Windows machine.

mise installs an older release, or the wrong one

What you see. Without a version, mise can pick a release that isn’t the newest, or one that has no morphirup for your machine. With the command from Installation, mise picked an older release that has no Windows ARM64 file:

mise ✗ github:finos/morphir-dotnet@0.4.0-alpha.4  943ms · failed: No matching asset found for platform windows-arm64

Without version_prefix=v,prerelease=true, it picks the plugin release plugin-v0.1.1, which has no morphirup:

mise ✗ github:finos/morphir-dotnet@plugin-v0.1.1  36ms · failed: No matching asset found for platform windows-arm64

Why. mise doesn’t always see the newest release: its list can leave out one published recently. Without those options, it takes the release that GitHub marks as latest, which in this repository is a plugin release.

What to do. Give mise the version. Add @<version> at the end, inside the quotes, as With mise shows. The versions are on the releases page, written without the v.

Setting up a toolchain

Linux: setup says the platform isn’t supported

What you see.

  "status": "refused",
  ...
    "code": "setup.platform_mismatch",
    "message": "Release setup requires an actual Windows x64 or ARM64 manager process."

The install script also warns, when it installs morphirup on Linux, that setup will refuse.

Why. setup doesn’t support Linux yet. morphirup itself installs and runs there, but there is no toolchain for it to set up.

What to do. Use Windows or a Mac with Apple silicon for now.

run or show says the Home needs setup

What you see. One of these, from morphirup run or morphirup show:

morphirup: local.setup_required: This Home was set up with local-toolchains 0.1.0-preview.3, and this morphirup reads only 0.1.0-preview.4. Run morphirup setup --yes again in this Home. ...
morphirup: local.platform_mismatch: Historical local execution requires a win-x64 manager process.
    "code": "local.io_refused",
    "message": "Local input or installed state could not be safely opened."

Why. morphirup keeps the toolchain in a folder called the Home: .morphir in your user folder, or the folder that MORPHIR_HOME names. The first message means the Home was set up by an older morphirup. The other two mean nothing has been set up in this Home yet. On Windows ARM64, run reports this case as a win-x64 problem, which is misleading. The fix is still to set up.

What to do. Run setup, then try again:

morphirup setup --yes

If you set MORPHIR_HOME, check that it names the same folder for setup and run.

Setup fails without a network

What you see. setup (or setup --dry-run) refuses with:

    "code": "acquisition.refused",
    "message": "The bounded release transfer was refused by transport or byte-integrity checks."

Why. Setup downloads the toolchain from GitHub, so it needs the network. A proxy that is set but doesn’t work causes the same error.

What to do. Run setup once while you are online, and check HTTPS_PROXY and HTTP_PROXY. Behind a company proxy or firewall, see Behind a proxy or firewall. After that, morphirup run -- compile and morphirup run -- generate need no network.

Setup says a path is too long (Windows)

Why. Windows limits a program’s path to 259 characters, and the Home’s folders add to the length.

What to do. Set MORPHIR_HOME to a short folder, such as C:\morphir-home, and run morphirup setup --yes again.

The release-checksum note

What you see. setup, show and run print this line on stderr:

morphirup: release-checksum delivery, NOT publisher-authenticated.

Why. It tells you how far the download was checked. Every file matched the checksums published with the release, so you have the files the release published, intact. The files are not signed, so the check doesn’t prove who published them.

What to do. Nothing. It is a notice, not an error. The morphirup reference describes the check in detail.

Validating IR

ir verify rejects the IR that compile wrote

What you see. morphir ir verify reports errors for a file that compile wrote. For format 4 it says:

Validation Result: ✗ INVALID
...
Schema Version: v3 (auto)
...
  Path: /formatVersion
  Message: Expected "3"

For a file compiled with --ir-version 3, it reports a very long list of errors.

Why. ir verify checks against the JSON schema of IR formats 1, 2 and 3. It doesn’t know format 4, and its schema doesn’t match the format 3 that compile writes.

What to do. Use morphir validate --input <file> instead, as Validating IR files shows.

validate can’t read the file

validate stops with exit code 2 when the file isn’t IR it can read. The message names the problem and where it is:

CauseMessage
The file is not complete JSONError: malformed-json: ...\broken.json: The input is not a complete JSON document.
formatVersion is a string, "3", not the number 3Error: unsupported-version: ...\bad-ir.json at $.formatVersion: Expected a classic IR version: integer 1, 2 or 3, or release 3.0.x or 3.1.x.
A tag has the wrong case, "public" for "Public"Error: decode: ...\case-ir.json at $.distribution[3].modules[0]: Invalid access 'public'.

Fix the file at the place the message names, and run validate again. The validate reference has the full list of input rules.

2.9 - Generate and run C#

Generate a typed Morphir library and execute it in a fresh C# consumer

Generate and run C#

Use a repository checkout, the Morphir CLI built from it, and SDK 10.0.403. Generation writes source files. Compilation is a separate step and requires the consumer’s compiler and Morphir.SDK.CSharp package.

The fixture exports a daily rate of 5,000 cents. These PowerShell commands create a fresh consumer and a local runtime feed without publishing a package:

dotnet pack src/Morphir.SDK.CSharp/Morphir.SDK.CSharp.csproj --configuration Release --output artifacts/csharp-feed -p:Version=0.0.0-local
dotnet new console --framework net10.0 --output artifacts/csharp-pricing
dotnet add artifacts/csharp-pricing package Morphir.SDK.CSharp --version 0.0.0-local --source "$PWD/artifacts/csharp-feed"
morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target csharp --output artifacts/csharp-pricing/Generated --option language-version=14 --option namespace=Pricing.Generated --json

Replace artifacts/csharp-pricing/Program.cs with:

using Pricing.Generated;

var cents = InventoryModule.DailyRate();
if (cents != 5000) throw new System.Exception("Unexpected daily rate");
System.Console.WriteLine(cents);

Then compile and execute:

dotnet build artifacts/csharp-pricing --configuration Release -p:LangVersion=14
dotnet run --project artifacts/csharp-pricing --configuration Release --no-build

The program prints 5000. Don’t edit the generated files: call their public API from your own code, and generate again when the model changes.

Without the namespace option, the generated code uses the model’s package name, here Car.Rental.

Keep the generated output in its own folder, apart from the input. Leave the .morphir-generated.json file in it: the next generate uses it to update only the files it owns and to keep yours. The generate reference covers the options, including language-version, and what happens when generation fails.

See C# troubleshooting for invalid options and model errors, and Generate F# from Morphir for F#.

2.10 - Generate F# from a Morphir model

Generate F# source from JSON or Ion libraries

Generate F# from a Morphir model

From a repository checkout, generate the shared daily-rate example:

morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target fsharp --output artifacts/generated-fsharp --json

The report lists Inventory.fs. The output contains that file and the .morphir-generated.json ownership manifest. Generate the same input again to receive an unchanged receipt without rewriting the file.

Classic JSON and Ion use the same command. For an Ion tree, pass its directory:

morphir generate --input tests/Fixtures/MorphirIon/trees/v3-daily-rate --target fsharp --output artifacts/generated-fsharp --json

Use the report’s artifact order when adding files to an F# project. Generation does not create the project or compile it. Keep handwritten files separate from owned artifact paths. See the command reference for the supported profile, options, exit codes and publication guarantees.

morphir generate --target fsharp uses the F# backend built into the CLI. The same generator also comes as a separate backend, morphir-fsharp-backend, which morphirup installs; see F# generation.

To run these commands without a checkout or .NET, see Your first model with morphirup.

3 - Guides

Comprehensive guides for using Morphir .NET

Generate model code with the Myriad F# reference, then follow the car rental tutorial through a complete rental and invoice.

Explore our guides to learn how to use Morphir .NET effectively.

Tutorials

Available Guides

  • Configuration - Current support and the planned shared configuration/project system
  • IR Modeling - Learn how to model Morphir IR in .NET
  • Serialization - Working with JSON serialization
  • Testing - Testing strategies and best practices

Best Practices

  • Immutability First: Always prefer immutable data structures
  • ADT Design: Use algebraic data types to make illegal states unrepresentable
  • Type Safety: Leverage C# 14 features for strong typing
  • Testing: Write comprehensive tests using TUnit and Reqnroll

3.1 - IR Modeling

Learn how to model Morphir IR in .NET

Overview

Morphir IR (Intermediate Representation) is the core data structure that represents your business logic. In Morphir .NET, we model the IR using C# record types and algebraic data types (ADTs).

Type Expressions

Type expressions represent the types in your Morphir model:

public abstract record TypeExpr
{
    public sealed record TInt() : TypeExpr;
    public sealed record TString() : TypeExpr;
    public sealed record TBool() : TypeExpr;
    public sealed record TTuple(IReadOnlyList<TypeExpr> Items) : TypeExpr;
    public sealed record TRecord(IReadOnlyDictionary<string, TypeExpr> Fields) : TypeExpr;
    public sealed record TFunc(TypeExpr Input, TypeExpr Output) : TypeExpr;
}

Value Expressions

Value expressions represent the actual values and computations:

public abstract record ValueExpr
{
    public sealed record Literal(LiteralValue Value) : ValueExpr;
    public sealed record Variable(string Name) : ValueExpr;
    public sealed record Lambda(string Parameter, TypeExpr ParameterType, ValueExpr Body) : ValueExpr;
    public sealed record Apply(ValueExpr Function, ValueExpr Argument) : ValueExpr;
}

Best Practices

  1. Use Records: Prefer record types for immutable data structures
  2. Pattern Matching: Use exhaustive pattern matching for ADTs
  3. Validation: Implement smart constructors for validated types
  4. Immutability: Keep all types immutable

Example

Here’s a complete example of modeling a simple function:

var addFunction = new ValueExpr.Lambda(
    Parameter: "x",
    ParameterType: new TypeExpr.TInt(),
    Body: new ValueExpr.Lambda(
        Parameter: "y",
        ParameterType: new TypeExpr.TInt(),
        Body: new ValueExpr.Apply(
            Function: new ValueExpr.Variable("+"),
            Argument: new ValueExpr.Tuple(new[]
            {
                new ValueExpr.Variable("x"),
                new ValueExpr.Variable("y")
            })
        )
    )
);

3.2 - Serialization

Working with JSON serialization in Morphir .NET

Overview

Morphir .NET provides JSON serialization support for Morphir IR, enabling interoperability with other Morphir tooling.

Basic Usage

Serializing IR to JSON

using Morphir.Core.IR;
using System.Text.Json;

var typeExpr = new TypeExpr.TInt();
var json = JsonSerializer.Serialize(typeExpr, new JsonSerializerOptions
{
    WriteIndented = true
});

Deserializing JSON to IR

var json = @"{""_tag"": ""TInt""}";
var typeExpr = JsonSerializer.Deserialize<TypeExpr>(json);

JSON Format

Morphir IR uses a tagged union format in JSON:

{
  "_tag": "TTuple",
  "items": [
    { "_tag": "TInt" },
    { "_tag": "TString" }
  ]
}

Custom Serialization

For custom serialization needs, you can implement your own converters:

public class CustomTypeExprConverter : JsonConverter<TypeExpr>
{
    // Implementation
}

Roundtrip Testing

Always test roundtrip serialization to ensure compatibility:

var original = new TypeExpr.TInt();
var json = JsonSerializer.Serialize(original);
var deserialized = JsonSerializer.Deserialize<TypeExpr>(json);
Assert.Equal(original, deserialized);

3.3 - Testing

Testing strategies and best practices for Morphir .NET

Overview

Morphir .NET supports multiple testing approaches to ensure code quality and correctness.

Unit Testing with TUnit

TUnit is the primary unit testing framework:

using TUnit.Assertions;
using TUnit.Core;

public class TypeExprTests
{
    [Test]
    public void TInt_Should_Be_Equal()
    {
        var type1 = new TypeExpr.TInt();
        var type2 = new TypeExpr.TInt();
        
        Assert.That(type1).IsEqualTo(type2);
    }
}

Behavior-Driven Development with Reqnroll

Reqnroll enables BDD-style testing:

Feature: Type Expression Creation
  Scenario: Create an integer type
    Given I want to create a type expression
    When I create a TInt
    Then it should be a valid type expression

Property-Based Testing

Use property-based testing for invariant validation:

[Property]
public bool RoundtripSerialization(TypeExpr typeExpr)
{
    var json = JsonSerializer.Serialize(typeExpr);
    var deserialized = JsonSerializer.Deserialize<TypeExpr>(json);
    return typeExpr.Equals(deserialized);
}

Contract Testing

Test compatibility with Morphir IR format:

[Test]
public void Should_Roundtrip_With_Morphir_Elm()
{
    // Load canonical IR sample
    var json = File.ReadAllText("samples/canonical.json");
    var ir = JsonSerializer.Deserialize<IR>(json);
    
    // Serialize back
    var roundtrip = JsonSerializer.Serialize(ir);
    
    // Verify compatibility
    Assert.That(roundtrip).IsValidJson();
}

Best Practices

  1. Exhaustive Testing: Test all ADT cases
  2. Edge Cases: Test boundary conditions
  3. Roundtrip Tests: Always test serialization roundtrips
  4. Property Tests: Use property-based testing for invariants
  5. Coverage: Maintain >= 80% code coverage

3.4 - Tutorial: car rental pricing from F# to C#

Model pricing rules in F#, compile them to Morphir IR, and call them from a C# app

The pricing team at Acme Car Rental writes its rules in F#. The booking team writes C#. Morphir IR is the contract between them. In this tutorial you build that pipeline.

graph LR
    A[F# rules] -->|compile| B[Morphir IR]
    B -->|validate| B
    B -->|generate| C[C# library]
    C --> D[Booking app]

You need the .NET SDK 10.0 or later and dotnet-morphir (install it). The finished files are in examples/car-rental-pricing.

1. Set up the project

The pricing rules live in an ordinary F# project.

mkdir -p acme/src && cd acme
<!-- Acme.CarRental.fsproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="src/Rates.fs" />
    <Compile Include="src/Quote.fs" />
  </ItemGroup>
</Project>

morphir.toml tells compile what to read, so you do not repeat options:

# morphir.toml
[project]
name = "acme/car-rental"

[frontend]
language = "fsharp"

[frontend.fsharp]
project_file = "Acme.CarRental.fsproj"

The project file gives the file order. F# needs it: Quote.fs uses Rates.fs.

2. Write the rules

// src/Rates.fs
namespace Acme.CarRental

module Rates =
    type VehicleClass =
        | Economy
        | Compact
        | Suv

    let dailyRate (vehicle: VehicleClass) : int =
        match vehicle with
        | Economy -> 4000
        | Compact -> 5000
        | Suv -> 8000
// src/Quote.fs
namespace Acme.CarRental

module Quote =
    type Rental =
        { Vehicle: Rates.VehicleClass
          Days: int
          Insurance: bool }

    let insurancePerDay = 1500

    let total (rental: Rental) : int =
        let rentalCost = Rates.dailyRate rental.Vehicle * rental.Days

        if rental.Insurance then
            rentalCost + insurancePerDay * rental.Days
        else
            rentalCost

Amounts are in cents.

3. Compile

dotnet-morphir compile
compile succeeded: 2 module(s), IR version 4, /work/acme/.morphir/out/compile.dest/morphir-ir.json; no diagnostics
/work/acme/.morphir/out/compile.dest/morphir-ir.json

4. Add a discount, and meet a refused construct

Product asks for 10% off rentals of a week or more. The first attempt, in src/Discount.fs, uses a guard:

namespace Acme.CarRental

module Discount =
    let weeklyDiscount (days: int) (total: int) : int =
        match days with
        | d when d >= 7 -> total * 90 / 100
        | _ -> total

Add it to the project file, after Quote.fs, and compile:

    <Compile Include="src/Discount.fs" />
src/Discount.fs:6:18: error fsharp.unsupported_pattern_guard: Pattern guards ('when') are not supported; a Morphir pattern has no condition. Match the case, then use 'if' in its body.
compile failed (source, exit code 1); 1 error(s): error fsharp.unsupported_pattern_guard

Morphir IR has no guarded patterns, so compile refuses the code and tells you what to do. No IR is written. Follow the hint:

// src/Discount.fs
namespace Acme.CarRental

module Discount =
    let weeklyDiscount (days: int) (total: int) : int =
        if days >= 7 then total * 90 / 100 else total

    let discountedTotal (rental: Quote.Rental) : int =
        weeklyDiscount rental.Days (Quote.total rental)
compile succeeded: 3 module(s), IR version 4, /work/acme/.morphir/out/compile.dest/morphir-ir.json; no diagnostics

Every refused construct has a code and a fix. See the F# coverage reference.

5. Use the rules from C#

dotnet new console --output BookingApp
dotnet add BookingApp package Morphir.SDK.CSharp --prerelease
dotnet-morphir generate --target csharp --output BookingApp/Generated

Replace BookingApp/Program.cs:

using Acme.CarRental;

var weekTrip = new QuoteModule.Rental(new RatesModule.VehicleClass.Compact(), 7, false);

Console.WriteLine($"Before discount: {QuoteModule.Total(weekTrip)}");
Console.WriteLine($"After discount:  {DiscountModule.DiscountedTotal(weekTrip)}");
dotnet run --project BookingApp
Before discount: 35000
After discount:  31500

Each F# module becomes a static C# class (Quote becomes QuoteModule). Records and union cases become classes with value equality. Do not edit the files in Generated; run generate again when the rules change.

6. Check the IR in CI

Use --json for a machine-readable report. The exit code is 0, 1 (a source error) or 2 (a setup error).

dotnet-morphir compile --json | jq '{success, modules, diagnostics}'
{
  "success": true,
  "modules": [
    "Rates",
    "Quote",
    "Discount"
  ],
  "diagnostics": []
}

To run the type validator, compile to classic IR version 3 and validate the result. The validator does not read IR version 4 yet.

dotnet-morphir compile --ir-version 3 -o dist
dotnet-morphir validate --input dist/morphir-ir.json
0 errors, 0 warnings, 0 info, 0 hints. Analyzers: 1 ran, 0 not run, 0 failed.

-o dist puts a copy of the IR in dist for other teams. It replaces only files that an earlier compile put there.

7. One command for the booking team

dotnet-morphir compile --target csharp --generate-output BookingApp/Generated
compile succeeded: 3 module(s), IR version 4, /work/acme/.morphir/out/compile.dest/morphir-ir.json, generated 3 artifact(s) with morphir.csharp to /work/acme/BookingApp/Generated; no diagnostics

What you built

StepCommand
F# rules to IRdotnet-morphir compile
IR to C#dotnet-morphir generate --target csharp --output <dir>
Both in one rundotnet-morphir compile --target csharp --generate-output <dir>
CI checkdotnet-morphir compile --json, dotnet-morphir validate --input <ir>

Next: compile F# with the native executable, or read the compile reference.

3.5 - Myriad F# generation reference

Configure classic Morphir JSON generation, module files and diagnostics

Morphir.Myriad translates classic Morphir library JSON into one F# file per module during a .NET 10 F# build. See getting started for the first run and the car rental tutorial for a complete business workflow.

Configure a consumer

Reference the package and add each model once:

<ItemGroup>
  <PackageReference Include="Morphir.Myriad" Version="YOUR_VERSION" PrivateAssets="all" />
  <MorphirModel Include="models/business.json" />
  <Compile Include="Program.fs" />
</ItemGroup>

Use the VersionOverride convention if your repository manages package versions centrally. The model path resolves from the consuming project directory. Paths containing spaces are supported. The package supplies its backend, plugin and build task dependencies, so these tools do not become runtime references in the application.

SettingMeaning
MorphirModel itemOne classic library JSON input
MorphirModel.OutputDirectoryOptional model-owned output directory, relative to the project directory or absolute
MorphirMyriadHostPathOptional path to the compatible Myriad host assembly

Only assign an output directory to one model. The build rejects duplicate output ownership and generated filename collisions. Distinct input models with identical basenames receive distinct default output identities.

Version and host contract

The plugin uses Myriad.Core 0.85.0, Fabulous.AST 1.10.0, Fantomas.Core 7.0.5 and the repository’s pinned FSharp.Core. The package bundles the real Myriad 0.85.0 host and launches it with .NET major-version roll forward. A cold first restore supplies the whole generation toolchain through the single package reference.

The host’s FSharp.Core must match the plugin’s runtime identity when formatting crosses their boundary. The bundled host is aligned with the plugin’s FSharp.Core and the coordinator uses an isolated runtime; it does not change the restored NuGet cache. Package contents include upstream attribution and third-party notices. Use the package’s normal build integration to obtain this setup automatically.

The generator attribute name is morphir-fsharp, its CLR type name is MorphirFSharpGenerator, and it accepts .json. It reads GeneratorContext.InputFilename and requires the MorphirModule additional parameter, a dot-separated canonical module path such as Inventory or Domain.Types. Each call returns only the selected module as Output.Source.

For direct host use with an appropriately aligned host runtime, the important arguments are:

dotnet --roll-forward LatestMajor <Myriad.dll> \
  --inputfile <model.json> --outputfile <module.fs> \
  --plugin <Morphir.Myriad.dll> \
  --generator-filter MorphirFSharpGenerator \
  --additionalparams MorphirModule=Inventory

In Myriad 0.85.0, --generator-filter selects the CLR type name. Passing the attribute name there does not select this generator. The normal MorphirModel build integration supplies the correct selection and output ordering for every module.

Maintain custom plugins

The legacy Elm migration scaffolder creates a .NET 10 plugin project for Myriad.Core 0.85.0:

dotnet fsi .claude/skills/elm-to-fsharp-guru/scripts/generate-myriad-plugin.fsx MyPlugin
dotnet build MyPlugin/MyPlugin.fsproj

The starter parses F# input and emits no code until you implement its attribute selection and module generation functions. Myriad 0.85.0 uses Fantomas.FCS.Syntax types. Ast.fromFilename parses the file asynchronously, and Ast.extractTypeDefn takes the resulting ParsedInput, not a filename. Output.Ast contains whole SynModuleOrNamespace nodes. Implement both ValidInputExtensions and Generate on IMyriadGenerator.

The scaffold copies runtime dependencies beside the plugin. When invoking the upstream host directly, align its FSharp.Core with the plugin as described above. Building the plugin alone does not configure a consumer’s code generation.

Renovate groups Myriad’s host, core, SDK and plugins with Fantomas.Core and Fabulous.AST in fsharp-codegen, separately from general .NET updates. Before merging an upgrade, run the generator suites and the package-consumer scenarios of the black-box tests:

dotnet run --project tests/Morphir.Myriad.Tests
dotnet run --project tests/Morphir.Internal.CodeGeneration.Tests
dotnet run --project tests/Morphir.Myriad.SystemTextJson.Tests
./build.sh --target Test --skip Restore Compile --test-group integration

The scaffold regression test builds a new plugin in a sandbox and runs it through the real host. The package-consumer scenarios (tests/Morphir.BlackBox.Tests/Integration/MyriadConsumer.feature, Integration/SystemTextJsonConsumer.feature and Integration/MyriadCarRental.feature) pack the plugins from the build, and check cold restores, repeat builds, failures, and consumers using both Morphir plugins.

Supported inputs and declarations

Input must contain an explicit integer formatVersion of 1, 2 or 3 and a matching classic Library distribution. Missing versions, version strings, fractional versions, other distributions and IR 4+ fail explicitly. A document tree is not an input to this plugin.

The backend translates aliases, generic variables, closed records, custom unions, tuples, functions and unit, along with classic values and patterns. These include literals, curried application, lambdas, bindings, local recursion, destructuring, conditions, ordered matches, record construction/access/update and field functions. Open extensible records are rejected instead of losing their row information.

Package and module qualification is retained. Record fields and values use Morphir camelCase; types and cases use PascalCase. F# keywords are escaped. Unsupported collisions and cycles between separate modules produce diagnostics. Private declarations retain restricted visibility in generated code.

Generation runs as a pipeline of parse, scope validation, type validation, constant folding and F# compilation. Folding replaces mapped SDK operations on literals with their result when it matches run-time behaviour, so add 2 3 appears as 5. A model’s own function named add is left alone, and so is a result outside the Int range. Folding does not change which models are accepted.

Initial SDK mappings

Morphir SDKGenerated representation
Basics.Int, Basics.Float, Basics.Boolint, float, bool
String.String, Char.Charstring, char
List.List aa list
Maybe.Maybe a, Just, Nothinga option, Some, None
Result.Result error value, Ok, ErrResult<value, error>, Ok, Error
Basics.Order and its casesMorphir.SDK.Order
Addition, subtraction, multiplication, divisionCorresponding curried F# operators
Equality and ordered comparisonsCorresponding F# comparisons
List.map, Maybe.map, Result.mapList.map, Option.map, Result.map

Unmapped SDK names and specification-only external references fail with their qualified name. Generated code using SDK-specific types such as Order needs the Morphir.SDK runtime reference; the supplied car rental model uses built-in mappings only. Int uses the existing .NET SDK’s 32-bit representation; an out-of-range whole-number literal is rejected rather than narrowed silently. The car rental sample uses whole days and integer cents within that range.

Output, regeneration and failure behavior

By default output lives under obj/<configuration>/<target-framework>/morphir/<model-identity>/. Each module has a distinct relative .fs path, and manifest.json records dependency order. Generated compilation inputs precede handwritten files. Empty libraries produce an empty manifest; no JSON is compiled as F#.

Ordinary builds plan the current model and run generation. Identical source and manifests are not rewritten. New output is staged and published after the model succeeds, and obsolete files owned by the prior manifest are removed. A failed generation stops compilation even when an older successful output exists. Design-time builds can read the last successful manifest. Build once normally after a clean checkout to establish it.

The coordinator isolates output by model, configuration and target framework. Avoid sharing an explicit output directory across unrelated builds. Model module/declaration maps are ordered deterministically, while argument lists and match branches retain their semantic order. Source contains no timestamps or absolute input paths.

Troubleshooting

SymptomCorrective step
Missing model pathCorrect MorphirModel.Include relative to the consumer project
unsupported-versionSupply explicit classic integer version 1, 2 or 3
Decode errorCheck the reported location against that version’s wire shape
Open-record diagnosticReplace the unsupported open row with a closed model type
Module dependency cycleRefactor the model’s cross-module cycle; files remain separate
Unmapped SDK/external symbolUse a supported mapping or provide an implementation in the model
unbound-variableBind the variable in the named value, or correct its name
type-mismatchCorrect the named value. Only definite conflicts between primitive types are reported, such as a string literal under a declared Int
fsharp-generationReview the rendering reason and IR declaration; constructs without an F# representation, such as empty unions, fail without partial output
Duplicate output directory or pathAssign distinct model ownership and names
Host method/assembly mismatchUse the documented pinned packages and automatic isolated host setup
Old plugin code after repackingUse a new development package version and restore it

Business errors such as InvalidDuration in the car rental example are results from generated functions. Generation diagnostics instead identify an input file and available JSON location or qualified symbol. Plugin diagnostic logging goes to stderr. IR 4+, document trees, generated codecs/lenses and a Morphir CLI generation command remain separate initiatives.

3.6 - Tutorial: compile F# with the native executable

Use the AOT morphir executable and the F# frontend extension, for build agents without the dotnet tool

Acme’s build agents run the native morphir executable: one file, fast to start, and no dotnet tool setup. The native executable has no F# compiler inside. It runs the F# frontend as an extension, a separate process that it talks to over the Morphir Extension Protocol (MEP).

sequenceDiagram
    participant M as morphir compile
    participant G as morphir-fsharp-frontend
    M->>G: initialize (agree IR versions)
    M->>G: compileModel (F# documents)
    G-->>M: Morphir IR
    M->>G: shutdown, exit

This tutorial uses the project from the F# pricing tutorial (examples/car-rental-pricing).

1. Install the executable and the extension

Download morphir for your platform from the releases page: morphir-linux-x64, morphir-linux-arm64, morphir-osx-arm64 or morphir-win-x64.exe.

VERSION=0.4.0-alpha.3   # the release you want
curl -fsSL -o morphir \
  "https://github.com/finos/morphir-dotnet/releases/download/v$VERSION/morphir-osx-arm64"
chmod +x morphir && mv morphir /usr/local/bin/

The F# frontend extension is a dotnet tool. It needs the .NET 10 runtime.

dotnet tool install --global Morphir.Frontend.FSharp.Extension --prerelease

2. Try it without the extension

cd acme
morphir compile
error compile.frontend_unavailable: No registered frontend reads the language 'fsharp'. This build registers no frontend. The self-contained and framework-dependent morphir and dotnet-morphir have a built-in F# frontend; this offering uses the F# frontend process delivery morphir-fsharp-frontend. Set [frontend.fsharp].extension in morphir.toml, or use --extension, to configure a frontend extension.
compile failed (configuration, exit code 2); 1 error(s): error compile.frontend_unavailable

3. Compile with the extension

morphir compile --extension morphir-fsharp-frontend
compile succeeded: 3 module(s), IR version 4, /work/acme/.morphir/out/compile.dest/morphir-ir.json; no diagnostics
/work/acme/.morphir/out/compile.dest/morphir-ir.json

The IR is the same, byte for byte, as dotnet-morphir compile writes. Classic IR versions work too:

morphir compile --extension morphir-fsharp-frontend --ir-version 2

4. Put it in morphir.toml

So that nobody has to remember the option, add the extension to the F# table:

[frontend.fsharp]
project_file = "Acme.CarRental.fsproj"
extension = "morphir-fsharp-frontend"
morphir compile --json | jq '{success, frontend, extension: .origins.extension.key}'
{
  "success": true,
  "frontend": "org.finos.morphir.fsharp-frontend (mep)",
  "extension": "frontend.fsharp.extension"
}

(mep) shows that the extension did the work. --extension on the command line wins over the key. The value can be a command on PATH, a path, or a .dll, which runs on the dotnet host.

5. When the extension is missing

error extension.unavailable: Extension morphir-fsharp-frontend did not start: An error occurred trying to start process 'morphir-fsharp-frontend' with working directory '/work/acme'. No such file or directory
compile failed (configuration, exit code 2); 1 error(s): error extension.unavailable
CodeFix
extension.unavailableInstall the extension, or check that it is on PATH.
extension.not_admittedRequest an IR version that the extension serves (1 to 4).
extension.protocol_errorUpdate the extension.
extension.timeoutRead the extension’s messages on stderr, then run again.

Limits

  • .fsi signature files: the extension reads .fs files only. Use dotnet-morphir for a project with signature files.
  • generate, validate and the rest of the commands are the same in both executables.

Next: the compile reference, or how the F# frontend runs as an extension.

3.7 - Myriad System.Text.Json generation

Generate trim-safe System.Text.Json converters and contexts for F# types at build time

Morphir.Myriad.SystemTextJson is the F# counterpart of the System.Text.Json source generator. The C# generator reads only C# code, so F# projects that must be trimmed or compiled with Native AOT had two choices: write each JSON codec by hand, or use a library that serializes through reflection. This Myriad plugin removes that choice. From your records, unions and enums it generates at build time:

  • one JsonConverter<T> for each type, which reads and writes with Utf8JsonReader and Utf8JsonWriter;
  • a context type that derives from JsonSerializerContext, with a JsonTypeInfo<T> member for each type and a Default instance.

The generated code uses no reflection. You call it through the normal JsonSerializer API, as you call a C# generated context. The union encodings use the names and the JSON of FSharp.SystemTextJson, so a type can move from it with no change to its JSON.

Set up a project

Reference the plugin and the annotations package, and declare one context for an input file:

<ItemGroup>
  <PackageReference Include="Morphir.Myriad.SystemTextJson" Version="YOUR_VERSION" PrivateAssets="all" />
  <PackageReference Include="Morphir.Myriad.SystemTextJson.Annotations" Version="YOUR_VERSION" />
</ItemGroup>
<ItemGroup>
  <Compile Include="Guide/Types.fs" />
  <FSharpJsonContext Include="Guide/Types.fs" Name="AppJson" Namespace="Guide.Json" Naming="CamelCase" />
  <Compile Include="Program.fs" />
</ItemGroup>

The input file must also be a Compile item. The build compiles the generated file right after it, so files after the input can use the context. The plugin package carries the Myriad 0.85.0 host; no separate command runs. The annotations package has no dependency, and nothing in it runs: the generator reads the attributes from the source.

A project can declare more than one context, also more than one for the same input file, each with its own name and settings.

Write and read values

For these types:

type Person = { Name: string; Age: int; Nickname: string option }

the context has the member AppJson.Default.Person:

let writePerson () =
    JsonSerializer.Serialize({ Name = "Ada"; Age = 36; Nickname = None }, AppJson.Default.Person)

writePerson () returns {"name":"Ada","age":36,"nickname":null}. JsonSerializer.Deserialize(json, AppJson.Default.Person) reads it back.

The context is a normal IJsonTypeInfoResolver. To use types from more than one context, combine them. Verify checks that the options can resolve every type that the context gets from other contexts, and names the first type that they cannot resolve:

let combine (other: IJsonTypeInfoResolver) =
    let options = JsonSerializerOptions(TypeInfoResolver = JsonTypeInfoResolver.Combine(AppJson.Default, other))
    AppJson.Default.Verify options
    options

Call Verify in a unit test for each set of options that you use, so that a missing context fails a test and not a production call.

Context settings

The custom metadata of a FSharpJsonContext item are the context settings. An unknown setting stops the build, so a misspelled setting does not go unseen.

SettingValuesDefault
NameOne F# identifier: the context typeRequired
NamespaceF# identifiers joined by dotsRequired
NamingAsIs, CamelCase, PascalCase, SnakeCaseLower, SnakeCaseUpper, KebabCaseLower, KebabCaseUpperAsIs
SkippableOptionFieldsAlways (leave out None), Never (write null)Never
UnknownMembersIgnore, RefuseIgnore
AllowNullFieldstrue, falsefalse
AllowNumbersFromStringstrue, falsefalse
MapFormatObjectOrArrayOfPairs, Object, ArrayOfPairsObjectOrArrayOfPairs
UnionEncodingBase encoding and flags, or a preset, joined by commasDefault
UnionTagNameThe name of the case memberCase
UnionFieldsNameThe name of the fields memberFields
UnionTagNamingA naming value for case namesAsIs
UnionFieldNamingA naming value for case field namesThe Naming value
UnionTagCaseInsensitivetrue, falsefalse

The defaults are those of FSharp.SystemTextJson.

Attributes

Attributes on a type, a case or a field override the context settings for that item only.

AttributeWhereUse
JsonRecordRecordNaming, SkippableOptionFields, UnknownMembers, AllowNullFields
JsonUnionUnionBaseEncoding, TagName, FieldsName, TagNaming, FieldNaming, UnknownMembers, TagCaseInsensitive
JsonNameField, caseThe JSON name. More names are read; the first is written. On a case, Field = "x" names the field x
JsonPropertyNameFieldThe System.Text.Json attribute, for one name
JsonParsedValueType, abbreviationA value that is written as text and read only through its parser
JsonIgnoreTypeTypeLeave the type out of the context
JsonDefaultValueRecord fieldUse a string, Boolean or numeric literal when the member is absent
JsonDefaultEmptyCollection fieldUse the empty collection when the member is absent
JsonDefaultValueProviderRecord fieldCall an accessible static method with no arguments when the member is absent
JsonDefaultJsonRecord fieldRead an explicit JSON literal through the generated field codec when absent
JsonOmitEmptyRecord collection fieldOmit an empty collection on writing
JsonOmitDefaultRecord bool field with JsonDefaultValueOmit the value on writing when it equals the default
JsonExtensionDataRecord map fieldCapture unknown members and write them beside the named members. This is the System.Text.Json attribute
JsonVersionAliasRecord version fieldNormalize an exact legacy member/version pair to the current member and version
JsonKnownPathsRecord string collectionAccept only the declared exact critical paths
JsonConverterType, fieldThe System.Text.Json attribute: use this converter
JsonNumberHandlingNumber fieldThe System.Text.Json attribute: AllowReadingFromString for this field

For example, this record uses snake case and refuses unknown members, and its colour member has two names:

[<JsonRecord(Naming = JsonNaming.SnakeCaseLower, UnknownMembers = JsonUnknownMembers.Refuse)>]
type Settings =
    { MaxItems: int
      [<JsonName("colour", "color")>]
      Colour: string }

It is written as {"max_items":3,"colour":"red"}, and {"max_items":3,"color":"red"} reads the same value.

Records

A record is a JSON object with one member for each field.

  • A field that is not an option is required unless it declares a missing-member default or extension data. A missing required member or disallowed null fails.
  • An option is null for None, or left out with SkippableOptionFields. A missing member reads as None.
  • A member that appears twice fails. An unknown member is skipped or refused, as UnknownMembers says.
  • AllowNullFields accepts null for a field whose type accepts null (string, arrays, ResizeArray, read-only interfaces, types from other contexts).

Defaults and empty collections

The wire-rule annotations below require a generator and annotations package built after 0.4.0-alpha.2.

These annotations opt into wire rules for a particular record field. Other fields keep their existing required-member rules:

type Diagnostic =
    { Message: string
      [<JsonDefaultEmpty; JsonOmitEmpty>]
      Related: string list
      [<JsonDefaultValue(false)>]
      Retryable: bool }

An absent Related reads as []; an empty Related is omitted on writing. An absent Retryable reads as false and is still written. JsonDefaultEmpty supports sequences and maps, using their configured encoding. JsonOmitEmpty supports the same shapes and does not itself make a missing member optional. To leave out a Boolean that equals its default, as for a flag that is written only when true, add JsonOmitDefault beside JsonDefaultValue.

JsonDefaultValue takes an ordinary string, Boolean or numeric literal. Strings are values, without JSON escaping: JsonDefaultValue("false") means the string "false", while JsonDefaultValue(false) means a Boolean. The generator checks primitive token kinds and numeric bounds, and the generated field codec reads the default using its normal encoding, including parsed-value parsers and union settings.

For decimals and other values that cannot be attribute arguments, name a static provider method:

type PaymentDefaults =
    static member Amount() : decimal = 12.50M

type Payment =
    { [<JsonDefaultValue("anonymous")>]
      Name: string
      [<JsonDefaultValue(3)>]
      Retries: int
      [<JsonDefaultValue(0.5)>]
      Ratio: float
      [<JsonDefaultValueProvider(typeof<PaymentDefaults>, nameof PaymentDefaults.Amount)>]
      Amount: decimal }

The generator emits a direct method call, without reflection or build-time execution of the provider. The F# compiler checks accessibility, static invocation, arguments and the return type against the field. Use a non-generic provider type and a method callable with no arguments; return a new mutable collection on each call when records must not share it. Provider methods should be deterministic and have no side effects. They run once for each absent member and do not run for explicit values or null. Null results follow the field’s null rules, and version and critical-path constraints are checked before returning the record. Providers supply typed values directly; they do not convert strings through a parser or JSON converter.

JsonDefaultJson("...") is the explicit escape hatch for defaults expressed in JSON, such as JsonDefaultJson("null") on a field configured to accept null. It takes one complete JSON value, read through the generated field codec. Malformed JSON fails generation; codec-specific validation runs when the default is used. Declare only one default annotation per field. Explicit null never selects any default.

These defaults apply to missing members while reading. They do not themselves omit values while writing. Like System.Text.Json, this generator does not interpret System.ComponentModel.DefaultValueAttribute. Constructor selection with JsonConstructor remains outside the current record and union scope; JsonPropertyName is supported as shown in the attribute table.

Open objects

Mark one Map<string, JsonElement> or IReadOnlyDictionary<string, JsonElement> record field with the System.Text.Json JsonExtensionData attribute. Unknown members enter that map and are written directly into the record object. The map is empty when no unknown member is present. There is no wrapper named after the field. Duplicate input members and output keys colliding with named members, their aliases, or an enclosing union tag are refused. Extension data cannot be combined with UnknownMembers.Refuse or additional default/omission annotations on the same field.

Versioned member migration

For a format whose version member changed its name, declare the exact legacy and current values. For example, the MEP claims contract uses:

type Claims =
    { [<JsonVersionAlias("statementVersion", "0.1.0-draft.1", "0.1.0-draft.2", CriticalMember = "critical")>]
      ClaimsVersion: string
      [<JsonDefaultEmpty; JsonOmitEmpty; JsonKnownPaths("claimsVersion", "protocolVersions", "capabilities", "capabilities.frontend")>]
      Critical: string list }

With camel-case context naming, a reader accepts statementVersion only with draft.1 and claimsVersion only with draft.2. Both names together fail. Legacy input normalizes to draft.2 and its exact statementVersion critical path becomes claimsVersion. A legacy list naming claimsVersion fails. Writers require the current version and write only claimsVersion.

CriticalMember is optional and names a JSON member, not an F# field. When set, it must name a string collection with JsonKnownPaths that allows the current version-member path and excludes the legacy path. The paths shown above illustrate a subset; declare every path your format understands. Known parents never permit unknown children. The version field can also be a JsonParsedValue type; its parser receives the normalized current text. If a version-alias field declares a scalar or JSON-literal default, it must encode the exact current-version string. Legacy, unknown, null and non-string literal defaults fail generation. Provider results are checked against the current version at deserialization. One version alias is allowed per record. These wire annotations apply to record fields.

Unions

The union encodings and flags have the names and meanings of FSharp.SystemTextJson. For this union:

type Shape =
    | NoArgs
    | OneArg of x: int
    | TwoArgs of a: int * b: string
EncodingNoArgsOneArg 1TwoArgs(3, "t")
AdjacentTag{"Case":"NoArgs"}{"Case":"OneArg","Fields":[1]}{"Case":"TwoArgs","Fields":[3,"t"]}
AdjacentTag, NamedFields{"Case":"NoArgs"}{"Case":"OneArg","Fields":{"x":1}}{"Case":"TwoArgs","Fields":{"a":3,"b":"t"}}
ExternalTag{"NoArgs":[]}{"OneArg":[1]}{"TwoArgs":[3,"t"]}
InternalTag["NoArgs"]["OneArg",1]["TwoArgs",3,"t"]
InternalTag, NamedFields{"Case":"NoArgs"}{"Case":"OneArg","x":1}{"Case":"TwoArgs","a":3,"b":"t"}
Untagged{}{"x":1}{"a":3,"b":"t"}
FlagEffect
UnwrapFieldlessTagsA case with no field is the string of its name: "NoArgs"
UnwrapSingleFieldCasesWith AdjacentTag or ExternalTag, one field is written alone: {"OneArg":1}
UnwrapRecordCasesThe members of a case’s single record field are written in place of the field
UnionFieldNamesFromTypesA field with no name is named after its type: Int32, String, Point
UnwrapOptionNone is null and Some x is x. Without it, Some x is a union case
UnwrapSingleCaseUnionsA union with one case and one field is written as that field
AllowUnorderedTagWhen reading, the case member can come after the fields
PresetFlags
DefaultAdjacentTag, UnwrapOption, UnwrapSingleCaseUnions, AllowUnorderedTag
NewtonsoftLikeAdjacentTag, AllowUnorderedTag
ThothLikeInternalTag, UnwrapFieldlessTags, AllowUnorderedTag
FSharpLuLikeExternalTag, UnwrapFieldlessTags, UnwrapOption, UnwrapSingleFieldCases, AllowUnorderedTag

A JsonUnion attribute sets the whole encoding of one union. This union is an object tagged by a kind member:

[<JsonUnion(BaseEncoding = (JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields), TagName = "kind")>]
type Shape =
    | Circle of radius: float
    | Square of side: float

Circle 1.5 is written as {"kind":"Circle","radius":1.5}.

An untagged union must let a reader tell its cases apart. The build fails when two cases share a member name, or when two cases have no field and UnwrapFieldlessTags is not set.

Additions to FSharp.SystemTextJson

Parsed values

A type with a private representation and a parser can be read only through the parser, so a reader never builds an invalid value:

[<JsonParsedValue(TextMember = "Value")>]
type Email =
    private
    | Email of string

    member this.Value = let (Email value) = this in value

    static member Parse(text: string) : Result<Email, string> =
        if text.Contains "@" then Ok(Email text) else Error $"'{text}' is not an e-mail address."
  • The writer writes the text member (ToString by default).
  • The reader calls Parse (string -> Result<T, 'E>), or a C#-style TryParse(string, out T) with UsesTryParse = true. An error fails with the parser’s text.
  • A parsed value can be a map key, so Map<Email, int> is a JSON object.
  • For a type from another assembly, put the attribute on an abbreviation in the input file: [<JsonParsedValue(TextMember = "Value")>] type ExtensionIdText = ExtensionId. Every use of ExtensionId in the context is then a parsed value.

Cases chosen by JSON token kind

SelectByTokenKind with Untagged chooses the case by the kind of the JSON value. Each case has one field, and no two cases have the same kind (string, number, boolean, array or object):

[<JsonUnion(BaseEncoding = (JsonUnionEncoding.Untagged ||| JsonUnionEncoding.SelectByTokenKind))>]
type RequestId =
    | Number of uint64
    | Text of string

18446744073709551615 reads as Number, and "req-1" reads as Text. Each one is written back as the same token.

Exact numbers

Each integer type is read with its own TryGet member, so the full range of uint64 and int64 is kept, and a fraction or a value out of range fails. A number in a JSON string is read only with AllowNumbersFromStrings on the context or JsonNumberHandling(JsonNumberHandling.AllowReadingFromString) on a number field.

Supported types

  • Primitives: string, bool, all integer types, float32, float, decimal, char, Guid, DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan and JsonElement, in the System.Text.Json formats.
  • option, voption, list, arrays, Set, ResizeArray, IReadOnlyList, IReadOnlyCollection, Map, IReadOnlyDictionary, tuples, and nesting of these. A type abbreviation of one of these is a context member.
  • Records, unions (also recursive), enums (as numbers), and abbreviations declared in the input file.
  • A type from another file or assembly is read and written through the options (options.GetTypeInfo), so its context must be in the options.

Generic types that you declare (type Page<'T> = ...) are not supported. Declare an abbreviation of a closed type instead. A user type with the name of a built-in type (Guid, Set, Single) is refused, because the generator reads types by their names.

Use with Native AOT

The generated code has no trim or AOT warning. An application that uses only generated contexts publishes with PublishAot=true. FSharp.Core itself gives two summary warnings (IL2104, IL3053) in every F# AOT application.

The F# compiler does not run the trim analyzers, so publish your application with Native AOT in CI to check it. morphir-dotnet does this with a probe that runs every shape and compares the native output with JIT.

Errors

At build time, the generator reports each problem at its file, line and column, with the code MSTJ0010, and stops the build:

Types.fs(3,6): error MSTJ0010: Page: generic types are not supported; declare an abbreviation of a closed type instead.

At run time, every failure is a JsonException. System.Text.Json adds the path and the position of the value. A failure inside a nested value names each member on the way to it:

In the member 'Head' of Household: In the member 'Age' of Person: A JSON integer in the Int32 range was expected.

Move from FSharp.SystemTextJson

  1. Keep your options as context settings: the encoding flags go in UnionEncoding, the naming policies in Naming, UnionTagNaming and UnionFieldNaming, and SkippableOptionFields and MapFormat keep their names.
  2. Replace JsonFSharpConverter attributes with JsonUnion or JsonRecord. JsonName keeps its meaning.
  3. Replace JsonSerializer.Serialize(value, options) with the type info overload, JsonSerializer.Serialize(value, AppJson.Default.Person), or set TypeInfoResolver on your options.
  4. Compare the JSON of your existing tests. For the same options, the JSON is the same; morphir-dotnet checks this for every base encoding, flag and preset.

3.8 - Car rental from model to executable F#

Transpile and run inventory, reservation, pickup, return and billing rules

This tutorial builds a car rental workflow from classic Morphir JSON and executes the generated F#. The business rules live in the model. The handwritten F# application supplies inputs, sequences operations and displays their results.

1. Build the example

Complete getting started, including local package creation, restore and run. Work from the repository root with the .NET SDK in global.json installed. No frontend compiler, database or payment service is required.

The input is examples/car-rental/models/car-rental-v3.json. Equivalent version 1 and 2 files are supplied. The optional examples/car-rental/tools/generate-model.fsx script authors the same model using classic Morphir expressions and regenerates the fixtures with dotnet fsi.

2. Follow the model modules

ModuleBusiness responsibility
DomainImmutable vehicle and invoice records, lifecycle unions and typed errors
InventorySample car, daily rate, lookup and availability
ReservationReserve a car and capture its daily rate
PickupStart the rental from a confirmed reservation
ReturnRecord billable days and release the car
BillingProduce one invoice after return
WorkflowCompose the complete rental journey

Inventory.car describes CAR-1 at 5000 cents per day. Its initial rental status is Unreserved. Domain.RentalStatus carries the captured rate through Confirmed, Active and Completed, then carries the invoice in Invoiced. A separate immutable pending-rental chain preserves completed rentals awaiting billing, and lastInvoice stores the most recently issued invoice.

The sample models one car’s current rental at a time. Inventory lookup accepts a list, but this example does not implement a reservation calendar. Use positive whole-day durations and integer cents. Taxes, discounts, payments and persistence are outside the example.

3. Inspect generation and compilation

Build the example:

dotnet build examples/car-rental/CarRental.fsproj --no-restore

Find the generated Domain.fs, Inventory.fs, Reservation.fs, Pickup.fs, Return.fs, Billing.fs and Workflow.fs under the project’s intermediate morphir directory. Open manifest.json to see compile order. Although the JSON lists dependent modules first, the build places their dependencies before them in F# compilation.

Each generated file preserves the Car.Rental package namespace and module names. Program.fs can call the generated functions directly. Generated files are disposable build output.

4. Reserve, pick up and return a car

The supplied driver follows these calls. requireOk extracts successful results and stops the console example if a rule unexpectedly fails.

open Car.Rental

let initial = Inventory.car
let found = Inventory.lookup "CAR-1" [ initial ]
let reserved = Reservation.reserve 3 initial |> requireOk
let pickedUp = Pickup.pickup reserved |> requireOk
let returned = Return.returnCar 3 pickedUp |> requireOk
let billed = Billing.bill returned |> requireOk

The generated functions return new records. They do not mutate initial or any earlier result.

OperationRental statusInventory availability
Initial inventoryUnreservedAvailable
ReserveConfirmed 5000Reserved
Pick upActive 5000Rented
Return after three daysCompleted (5000, 3)Available
BillInvoiced invoiceAvailable

Reservation captures the rate so a later rate change does not alter this rental’s price. Return validates the actual billable day count, completes the rental, preserves its billing obligation and releases the car. A new reservation can start before the previous rental is billed; billing that completed rental preserves the new reservation and the original captured rate. The driver checks this case as well. The composed generated function Workflow.rent 3 performs the basic journey; the driver checks that both routes agree.

5. Read the invoice and rejected operations

Billing.invoice billed returns Some invoice with dailyRate = 5000, days = 3 and totalCents = 15000. The amount is 5000 × 3, expressed in cents throughout.

The driver also verifies these generated error paths:

AttemptTyped business error
Reserve the reserved carUnavailable
Pick up the initial carReservationRequired
Return the initial carActiveRentalRequired
Reserve for zero daysInvalidDuration
Return for zero billable daysInvalidDuration
Bill before returnReturnRequired
Bill an already billed rentalAlreadyBilled

Negative durations follow the same invalid-duration rule. Each rejected operation leaves its input state unchanged. These are business results returned by generated F#, so the application can handle them without parsing build diagnostics.

6. Change the model and regenerate

In models/car-rental-v3.json, locate the inventory module’s dailyRate value definition. Its body is a literal containing 5000. Change that literal to 6000. This is the single sample rate definition; references to it stay intact.

Run a fresh journey:

dotnet run --project examples/car-rental/CarRental.fsproj --no-restore

The output now includes dailyRate=6000 and invoice=18000. The build regenerated Inventory.fs; the new reservation captured 6000 cents per day and the generated billing function charged three days. No generated source or handwritten business rule changed.

Restore the sample literal to 5000 afterwards. To make a permanent authoring change across every fixture, edit the integer 5000 daily-rate expression in tools/generate-model.fsx and run:

dotnet fsi examples/car-rental/tools/generate-model.fsx

The BDD suite repeats this rate-change exercise in a disposable copy and verifies the invoice total, preventing the tutorial from drifting away from the implementation.

7. Try the other classic versions

dotnet run --project examples/car-rental/CarRental.fsproj --no-restore \
  -p:MorphirModelFile=models/car-rental-v1.json
dotnet run --project examples/car-rental/CarRental.fsproj --no-restore \
  -p:MorphirModelFile=models/car-rental-v2.json

Both runs produce the original 15000-cent invoice when their checked-in rates remain 5000. The fixture schemas differ, including v1 access wrappers and version-specific tags; the backend decodes each version before generating equivalent F#.

Troubleshooting

An Error Domain.InvalidDuration is a business rejection. Supply a positive day count. An unsupported-version build error means the JSON format is outside versions 1, 2 and 3; use a supported classic file. A missing-model or decoding diagnostic names the input path and available location; correct the configured path or model structure and rebuild.

Generation failures block compilation even if old generated files exist. Check the plugin reference for host, dependency, output ownership and module-cycle diagnostics. IR 4+, document trees and a Morphir CLI generation command are separate initiatives.

4 - Morphir IR Specification

The complete Morphir IR specification and JSON schemas

Morphir IR Specification

This section contains the Morphir IR (Intermediate Representation) specification and related schema files.

Contents

  • Morphir IR Specification: The complete Morphir IR specification document, describing the structure, semantics, and usage of the Morphir IR format.

  • JSON Schemas: JSON schema definitions for all supported format versions of the Morphir IR:

    • morphir-ir-v3.yaml: Current format version (v3)
    • morphir-ir-v2.yaml: Format version 2
    • morphir-ir-v1.yaml: Format version 1

Purpose

This specification section serves as the authoritative reference for:

  • Implementers: Building tools that generate, consume, or transform Morphir IR
  • Developers: Working with Morphir IR in .NET and other platforms
  • LLMs: Providing context for AI tools working with Morphir
  • Tooling: Validating and processing Morphir IR JSON files

4.1 - Morphir IR Specification

Complete specification of the Morphir Intermediate Representation (IR)

Morphir IR Specification

Overview

The Morphir Intermediate Representation (IR) is a language-independent, platform-agnostic representation of business logic and domain models. It serves as a universal format that captures the semantics of functional programs, enabling them to be transformed, analyzed, and executed across different platforms and languages.

Purpose

The Morphir IR specification defines:

  • Building blocks: Core concepts and data structures that form the IR
  • Relationships: How different components of the IR relate to and reference each other
  • Semantics: The meaning and behavior of each construct

This specification is designed to:

  • Guide implementers building tools that generate, consume, or transform Morphir IR
  • Provide context for Large Language Models (LLMs) working with Morphir
  • Serve as the authoritative reference for the Morphir IR structure

Design Principles

The Morphir IR follows these key principles:

  • Functional: All logic is expressed as pure functions without side effects
  • Type-safe: Complete type information is preserved throughout the IR
  • Hierarchical: Code is organized in a hierarchical namespace (Package → Module → Type/Value)
  • Naming-agnostic: Names are stored in a canonical format independent of any specific naming convention
  • Explicit: All references are fully-qualified to eliminate ambiguity

Core Concepts

Naming

Morphir uses a sophisticated naming system that is independent of any specific naming convention (camelCase, snake_case, etc.). This allows the same IR to be rendered in different conventions for different platforms.

Name

A Name represents a human-readable identifier made up of one or more words.

  • Structure: A list of lowercase word strings
  • Purpose: Serves as the atomic unit for all identifiers
  • Example: ["value", "in", "u", "s", "d"] can be rendered as:
    • valueInUSD (camelCase)
    • ValueInUSD (TitleCase)
    • value_in_USD (snake_case)

Path

A Path represents a hierarchical location in the IR structure.

  • Structure: A list of Names
  • Purpose: Identifies packages and modules within the hierarchy
  • Example: [["morphir"], ["s", "d", "k"], ["string"]] represents the path to the String module

Qualified Name (QName)

A Qualified Name uniquely identifies a type or value within a package.

  • Structure: A tuple of (module path, local name)
  • Components:
    • Module path: The Path to the module
    • Local name: The Name of the type or value within that module
  • Purpose: Identifies items relative to a package

Fully-Qualified Name (FQName)

A Fully-Qualified Name provides a globally unique identifier for any type or value.

  • Structure: A tuple of (package path, module path, local name)
  • Components:
    • Package path: The Path to the package
    • Module path: The Path to the module within the package
    • Local name: The Name of the type or value
  • Purpose: Enables unambiguous references across package boundaries

Attributes and Annotations

The IR supports extensibility through attributes that can be attached to various nodes:

  • Type attributes (ta): Extra information attached to type nodes (e.g., source location, type inference results)
  • Value attributes (va): Extra information attached to value nodes (e.g., source location, inferred types)

When no additional information is needed, the unit type () is used as a placeholder.

Access Control

AccessControlled

An AccessControlled wrapper manages visibility of types and values.

  • Structure: { access, value }
  • Access levels:
    • Public: Visible to external consumers of the package
    • Private: Only visible within the package
  • Purpose: Controls what parts of a package are exposed in its public API

Documented

A Documented wrapper associates documentation with IR elements.

  • Structure: { doc, value }
  • Components:
    • doc: A string containing documentation text
    • value: The documented element
  • Purpose: Preserves documentation for types and values

Distribution

A Distribution represents a complete, self-contained package of Morphir code with all its dependencies.

Structure

Currently, Morphir supports a single distribution type: Library

A Library distribution contains:

  • Package name: The globally unique identifier for the package (like NPM package name or Maven GroupId:ArtifactId)
  • Dependencies: A dictionary mapping package names to their specifications
    • Dependencies only contain type signatures (specifications), not implementations
  • Package definition: The complete implementation of the package
    • Contains all module definitions, including private modules
    • Includes both type signatures and implementations

Purpose

A distribution is:

  • The output of the Morphir compilation process (e.g., morphir-elm make)
  • A complete unit that can be executed, analyzed, or transformed
  • Self-contained with all dependency information included

Package

A Package is a collection of modules that are versioned and distributed together. It corresponds to what package managers like NPM, NuGet, Maven, or pip consider a package.

Package Specification

A Package Specification provides the public interface of a package.

Structure:

  • modules: A dictionary mapping module names (Paths) to Module Specifications

Characteristics:

  • Contains only publicly exposed modules
  • Types are only included if they are public
  • Values are only included if they are public
  • No implementation details are included

Package Definition

A Package Definition provides the complete implementation of a package.

Structure:

  • modules: A dictionary mapping module names (Paths) to AccessControlled Module Definitions

Characteristics:

  • Contains all modules (both public and private)
  • All types are included (both public and private)
  • All values are included with their implementations
  • Each module is wrapped in AccessControlled to indicate its visibility

Package Name

A Package Name is the globally unique identifier for a package.

  • Structure: A Path (list of Names)
  • Examples: [["morphir"], ["s", "d", "k"]], [["my"], ["company"], ["models"]]
  • Purpose: Uniquely identifies a package across all Morphir systems

Module

A Module groups related types and values together, similar to packages in Java or namespaces in other languages.

Module Specification

A Module Specification provides the public interface of a module.

Structure:

  • types: Dictionary of type names to Documented Type Specifications
  • values: Dictionary of value names to Documented Value Specifications
  • doc: Optional documentation string for the module

Characteristics:

  • Only includes publicly exposed types and values
  • Contains type signatures but no implementations
  • Documentation is preserved from the source

Module Definition

A Module Definition provides the complete implementation of a module.

Structure:

  • types: Dictionary of type names to AccessControlled, Documented Type Definitions
  • values: Dictionary of value names to AccessControlled, Documented Value Definitions
  • doc: Optional documentation string for the module

Characteristics:

  • Includes all types and values (public and private)
  • Contains complete implementations
  • Each type and value is wrapped in AccessControlled to indicate visibility
  • Documentation is preserved from the source

Module Name

A Module Name uniquely identifies a module within a package.

  • Structure: A Path (list of Names)
  • Examples: [["morphir"], ["i", "r"], ["type"]], [["my"], ["module"]]

Qualified Module Name

A Qualified Module Name provides a globally unique module identifier.

  • Structure: A tuple of (package path, module path)
  • Purpose: Enables unambiguous module references across packages

Type System

The Morphir type system is based on functional programming principles, similar to languages like Elm, Haskell, or ML.

Type Expressions

A Type is a recursive tree structure representing type expressions. Each node can have type attributes attached.

Variable

Represents a type variable (generic parameter).

  • Structure: Variable a Name
  • Components:
    • a: Type attribute
    • Name: The variable name
  • Example: The a in List a
  • Purpose: Enables generic/polymorphic types

Reference

A reference to another type or type alias.

  • Structure: Reference a FQName (List Type)
  • Components:
    • a: Type attribute
    • FQName: Fully-qualified name of the referenced type
    • List Type: Type arguments (for generic types)
  • Examples:
    • String → Reference a (["morphir"], ["s", "d", "k"], ["string"]) []
    • List Int → Reference a (["morphir"], ["s", "d", "k"], ["list"]) [intType]
  • Purpose: Refers to built-in types, custom types, or type aliases

Tuple

A composition of multiple types in a fixed order.

  • Structure: Tuple a (List Type)
  • Components:
    • a: Type attribute
    • List Type: Element types in order
  • Examples:
    • (Int, String) → Tuple a [intType, stringType]
    • (Int, Int, Bool) → Tuple a [intType, intType, boolType]
  • Notes:
    • Zero-element tuple is equivalent to Unit
    • Single-element tuple is equivalent to the element type itself
  • Purpose: Represents product types with positional access

Record

A composition of named fields with their types.

  • Structure: Record a (List Field)
  • Components:
    • a: Type attribute
    • List Field: List of field definitions
  • Field structure: { name: Name, tpe: Type }
  • Example: { firstName: String, age: Int }
  • Notes:
    • Field order is preserved but not semantically significant
    • All fields are required (no optional fields)
  • Purpose: Represents product types with named field access

ExtensibleRecord

A record type that can be extended with additional fields.

  • Structure: ExtensibleRecord a Name (List Field)
  • Components:
    • a: Type attribute
    • Name: Type variable representing the extension
    • List Field: Known fields
  • Example: { a | firstName: String, age: Int } means “type a with at least these fields”
  • Purpose: Enables flexible record types that can be extended

Function

Represents a function type.

  • Structure: Function a Type Type
  • Components:
    • a: Type attribute
    • First Type: Argument type
    • Second Type: Return type
  • Examples:
    • Int -> String → Function a intType stringType
    • Int -> Int -> Bool → Function a intType (Function a intType boolType)
  • Notes:
    • Multi-argument functions are represented via currying (nested Function types)
  • Purpose: Represents the type of functions and lambdas

Unit

The type with exactly one value.

  • Structure: Unit a
  • Components:
    • a: Type attribute
  • Purpose: Placeholder where a type is needed but the value is unused
  • Corresponds to void in some languages

Type Specifications

A Type Specification defines the interface of a type without implementation details.

TypeAliasSpecification

An alias for another type.

  • Structure: TypeAliasSpecification (List Name) Type
  • Components:
    • List Name: Type parameters
    • Type: The aliased type expression
  • Example: type alias UserId = String
  • Purpose: Provides a meaningful name for a type expression

OpaqueTypeSpecification

A type with unknown structure.

  • Structure: OpaqueTypeSpecification (List Name)
  • Components:
    • List Name: Type parameters
  • Characteristics:
    • Structure is hidden from consumers
    • Cannot be automatically serialized
    • Values can only be created and manipulated via provided functions
  • Purpose: Encapsulates implementation details

CustomTypeSpecification

A tagged union type (sum type).

  • Structure: CustomTypeSpecification (List Name) Constructors
  • Components:
    • List Name: Type parameters
    • Constructors: Dictionary of constructor names to their arguments
  • Constructor arguments: List (Name, Type) - list of named, typed arguments
  • Example: type Result e a = Ok a | Err e
  • Purpose: Represents choice between multiple alternatives

DerivedTypeSpecification

A type with platform-specific representation but known serialization.

  • Structure: DerivedTypeSpecification (List Name) Details
  • Details contain:
    • baseType: The type used for serialization
    • fromBaseType: FQName of function to convert from base type
    • toBaseType: FQName of function to convert to base type
  • Example: A LocalDate might serialize to/from String with conversion functions
  • Purpose: Enables platform-specific types while maintaining serialization capability

Type Definitions

A Type Definition provides the complete implementation of a type.

TypeAliasDefinition

Complete definition of a type alias.

  • Structure: TypeAliasDefinition (List Name) Type
  • Components:
    • List Name: Type parameters
    • Type: The complete type expression being aliased
  • Same as specification (aliases have no hidden implementation)

CustomTypeDefinition

Complete definition of a custom type.

  • Structure: CustomTypeDefinition (List Name) (AccessControlled Constructors)
  • Components:
    • List Name: Type parameters
    • AccessControlled Constructors: Constructor definitions with visibility control
  • If constructors are Private → specification becomes OpaqueTypeSpecification
  • If constructors are Public → specification becomes CustomTypeSpecification
  • Purpose: Allows hiding constructors while exposing the type

Value System

Values represent both data and logic in Morphir. All computations are expressed as value expressions.

Value Expressions

A Value is a recursive tree structure representing computations. Each node can have type and value attributes.

Literal

A literal constant value.

  • Structure: Literal va Literal
  • Components:
    • va: Value attribute
    • Literal: The literal value
  • Supported literal types:
    • BoolLiteral: Boolean values (True, False)
    • CharLiteral: Single characters ('a', 'Z')
    • StringLiteral: Text strings ("hello")
    • WholeNumberLiteral: Integers (42, -17)
    • FloatLiteral: Floating-point numbers (3.14, -0.5)
    • DecimalLiteral: Arbitrary-precision decimals
  • Purpose: Represents constant data

Constructor

Reference to a custom type constructor.

  • Structure: Constructor va FQName
  • Components:
    • va: Value attribute
    • FQName: Fully-qualified name of the constructor
  • If the constructor has arguments, it will be wrapped in Apply nodes
  • Example: Just in Maybe a, Ok in Result e a
  • Purpose: Creates tagged union values

Tuple

A tuple value with multiple elements.

  • Structure: Tuple va (List Value)
  • Components:
    • va: Value attribute
    • List Value: Element values in order
  • Example: (42, "hello", True)
  • Purpose: Groups multiple values together with positional access

List

A list of values.

  • Structure: List va (List Value)
  • Components:
    • va: Value attribute
    • List Value: List elements
  • Example: [1, 2, 3, 4]
  • Purpose: Represents homogeneous sequences

Record

A record value with named fields.

  • Structure: Record va (Dict Name Value)
  • Components:
    • va: Value attribute
    • Dict Name Value: Dictionary mapping field names to values
  • Example: { firstName = "John", age = 30 }
  • Purpose: Represents structured data with named field access

Variable

Reference to a variable in scope.

  • Structure: Variable va Name
  • Components:
    • va: Value attribute
    • Name: Variable name
  • Example: References to function parameters or let-bound variables
  • Purpose: Accesses values bound in the current scope

Reference

Reference to a defined value (function or constant).

  • Structure: Reference va FQName
  • Components:
    • va: Value attribute
    • FQName: Fully-qualified name of the referenced value
  • Example: Morphir.SDK.List.map, Basics.add
  • Purpose: Invokes or references defined functions and constants

Field

Field access on a record.

  • Structure: Field va Value Name
  • Components:
    • va: Value attribute
    • Value: The record expression
    • Name: Field name to access
  • Example: user.firstName → Field va (Variable va ["user"]) ["first", "name"]
  • Purpose: Extracts a specific field from a record

FieldFunction

A function that extracts a field.

  • Structure: FieldFunction va Name
  • Components:
    • va: Value attribute
    • Name: Field name
  • Example: .firstName creates a function \r -> r.firstName
  • Purpose: Creates a field accessor function

Apply

Function application.

  • Structure: Apply va Value Value
  • Components:
    • va: Value attribute
    • First Value: The function
    • Second Value: The argument
  • Multi-argument calls are represented via currying (nested Apply nodes)
  • Example: add 1 2 → Apply va (Apply va (Reference va add) (Literal va 1)) (Literal va 2)
  • Purpose: Invokes functions with arguments

Lambda

Anonymous function (lambda abstraction).

  • Structure: Lambda va Pattern Value
  • Components:
    • va: Value attribute
    • Pattern: Pattern matching the input
    • Value: Function body
  • Example: \x -> x + 1 → Lambda va (AsPattern va (WildcardPattern va) ["x"]) (body)
  • Purpose: Creates inline functions

LetDefinition

A let binding introducing a single value.

  • Structure: LetDefinition va Name Definition Value
  • Components:
    • va: Value attribute
    • Name: Binding name
    • Definition: Value definition being bound
    • Value: Expression where the binding is in scope
  • Example: let x = 5 in x + x
  • Purpose: Introduces local bindings

LetRecursion

Mutually recursive let bindings.

  • Structure: LetRecursion va (Dict Name Definition) Value
  • Components:
    • va: Value attribute
    • Dict Name Definition: Multiple bindings that can reference each other
    • Value: Expression where the bindings are in scope
  • Purpose: Enables mutual recursion between bindings

Destructure

Pattern-based destructuring.

  • Structure: Destructure va Pattern Value Value
  • Components:
    • va: Value attribute
    • Pattern: Pattern to match
    • First Value: Expression to destructure
    • Second Value: Expression where extracted variables are in scope
  • Example: let (x, y) = point in ...
  • Purpose: Extracts values from structured data

IfThenElse

Conditional expression.

  • Structure: IfThenElse va Value Value Value
  • Components:
    • va: Value attribute
    • First Value: Condition
    • Second Value: Then branch
    • Third Value: Else branch
  • Example: if x > 0 then "positive" else "non-positive"
  • Purpose: Conditional logic

PatternMatch

Pattern matching with multiple cases.

  • Structure: PatternMatch va Value (List (Pattern, Value))
  • Components:
    • va: Value attribute
    • Value: Expression to match against
    • List (Pattern, Value): List of pattern-branch pairs
  • Example: case maybeValue of Just x -> x; Nothing -> 0
  • Purpose: Conditional logic based on structure

UpdateRecord

Record update expression.

  • Structure: UpdateRecord va Value (Dict Name Value)
  • Components:
    • va: Value attribute
    • Value: The record to update
    • Dict Name Value: Fields to update with new values
  • Example: { user | age = 31 }
  • Notes: This is copy-on-update (immutable)
  • Purpose: Creates a modified copy of a record

Unit

The unit value.

  • Structure: Unit va
  • Components:
    • va: Value attribute
  • Purpose: Represents the single value of the Unit type

Patterns

Patterns are used for destructuring and filtering values. They appear in lambda, let destructure, and pattern match expressions.

WildcardPattern

Matches any value without binding.

  • Structure: WildcardPattern a
  • Syntax: _ in source languages
  • Purpose: Ignores a value

AsPattern

Binds a name to a value matched by a nested pattern.

  • Structure: AsPattern a Pattern Name
  • Components:
    • a: Pattern attribute
    • Pattern: Nested pattern
    • Name: Variable name to bind
  • Syntax: pattern as name in source languages
  • Special case: Simple variable binding is AsPattern a (WildcardPattern a) name
  • Purpose: Captures matched values

TuplePattern

Matches a tuple by matching each element.

  • Structure: TuplePattern a (List Pattern)
  • Components:
    • a: Pattern attribute
    • List Pattern: Patterns for each tuple element
  • Example: (x, y) matches a 2-tuple
  • Purpose: Destructures tuples

ConstructorPattern

Matches a specific type constructor and its arguments.

  • Structure: ConstructorPattern a FQName (List Pattern)
  • Components:
    • a: Pattern attribute
    • FQName: Fully-qualified constructor name
    • List Pattern: Patterns for constructor arguments
  • Example: Just x matches Just with pattern x
  • Purpose: Destructures and filters tagged unions

EmptyListPattern

Matches an empty list.

  • Structure: EmptyListPattern a
  • Syntax: [] in source languages
  • Purpose: Detects empty lists

HeadTailPattern

Matches a non-empty list by head and tail.

  • Structure: HeadTailPattern a Pattern Pattern
  • Components:
    • a: Pattern attribute
    • First Pattern: Matches the head element
    • Second Pattern: Matches the tail (remaining list)
  • Syntax: x :: xs in source languages
  • Purpose: Destructures lists recursively

LiteralPattern

Matches an exact literal value.

  • Structure: LiteralPattern a Literal
  • Components:
    • a: Pattern attribute
    • Literal: The exact value to match
  • Example: 42, "hello", True
  • Purpose: Filters by exact value

UnitPattern

Matches the unit value.

  • Structure: UnitPattern a
  • Purpose: Matches the Unit value

Value Specifications

A Value Specification defines the type signature of a value or function.

Structure:

  • inputs: List of (Name, Type) pairs representing function parameters
  • output: The return type

Characteristics:

  • Contains only type information, no implementation
  • Multi-argument functions list all parameters
  • Zero-argument values (constants) have empty inputs list

Example: add : Int -> Int -> Int becomes:

{ inputs = [("a", Int), ("b", Int)]
, output = Int
}

Value Definitions

A Value Definition provides the complete implementation of a value or function.

Structure:

  • inputTypes: List of (Name, va, Type) tuples for function parameters
    • Name: Parameter name
    • va: Value attribute for the parameter
    • Type: Parameter type
  • outputType: The return type
  • body: The value expression implementing the logic

Characteristics:

  • Contains both type information and implementation
  • Parameters are extracted from nested lambdas when possible
  • Body contains the actual computation

Relationships Between Concepts

Hierarchical Structure

Distribution
  └─ Package (with dependencies)
      └─ Module
          ├─ Types
          │   └─ Type Definition/Specification
          └─ Values
              └─ Value Definition/Specification

Specifications vs Definitions

  • Specifications: Public interface only

    • Used for dependencies
    • Contain type signatures only
    • Expose only public items
  • Definitions: Complete implementation

    • Used for the package being compiled
    • Contain all details
    • Include both public and private items

Conversion Flow

Definition → Specification
  - Package Definition → Package Specification
  - Module Definition → Module Specification  
  - Type Definition → Type Specification
  - Value Definition → Value Specification

Specifications can be created with or without private items:

  • definitionToSpecification: Public items only
  • definitionToSpecificationWithPrivate: All items included

Reference Resolution

References in the IR are always fully-qualified:

  1. Within expressions: References use FQName (package, module, local name)
  2. Within modules: Items use local Names (looked up in module context)
  3. Within packages: Modules use Paths (looked up in package context)

This eliminates ambiguity and enables:

  • Easy dependency tracking
  • Cross-package linking
  • Independent processing of modules

Semantics

Type System Semantics

  • Type Safety: All values have types; type checking ensures correctness
  • Polymorphism: Type variables enable generic programming
  • Structural Typing: Records and tuples are compared structurally
  • Nominal Typing: Custom types are compared by name
  • Immutability: All values are immutable; updates create new values

Value Evaluation Semantics

  • Pure Functions: All functions are pure (no side effects)
  • Eager Evaluation: Arguments are evaluated before function application
  • Pattern Matching: Patterns are tested in order; first match wins
  • Scope Rules:
    • Lambda parameters are in scope in the lambda body
    • Let bindings are in scope in the let expression body
    • Pattern variables are in scope in the associated branch

Access Control Semantics

  • Public: Visible in package specifications; accessible to consumers
  • Private: Only visible within package definition; not exposed
  • Custom type constructors: Can be public (pattern matching allowed) or private (opaque type)

Usage Guidelines for Tool Implementers

Generating IR

When generating Morphir IR from source code:

  1. Preserve names in canonical form: Convert all identifiers to lowercase word lists
  2. Use fully-qualified references: Always include package and module paths
  3. Maintain access control: Mark public vs private appropriately
  4. Extract lambdas into function parameters: Use the inputTypes field instead of nested lambdas where possible
  5. Preserve documentation: Include doc strings from source

Consuming IR

When consuming Morphir IR:

  1. Respect access control: Only access public items from dependencies
  2. Resolve references: Use the distribution to look up type and value definitions
  3. Handle attributes: Be prepared for different attribute types or use unit type
  4. Follow naming conventions: Use Name conversion utilities for target platform
  5. Process hierarchically: Start from Distribution → Package → Module → Types/Values

Transforming IR

When transforming Morphir IR:

  1. Preserve structure: Maintain the hierarchical organization
  2. Update references consistently: If you rename items, update all references
  3. Maintain type correctness: Ensure transformations preserve type safety
  4. Handle both specifications and definitions: Transform both forms consistently
  5. Preserve attributes: Carry forward attributes unless explicitly changing them

JSON Schema Specifications

To support tooling, validation, and interoperability, formal JSON schemas are provided for all supported format versions of the Morphir IR. These schemas are defined in YAML format for readability and include comprehensive documentation.

Available Schemas

  • Format Version 3 (Current): The latest format version, which uses capitalized constructor tags (e.g., "Library", "Public", "Variable").

  • Format Version 2: Uses capitalized distribution and type tags (e.g., "Library", "Public", "Variable") but lowercase value and pattern tags (e.g., "apply", "lambda", "as_pattern").

  • Format Version 1: The original format version, which uses lowercase tags throughout (e.g., "library", "public") and a different module structure where modules have name and def fields.

Key Differences Between Versions

Format Version 1 → 2

  • Distribution tag: Changed from "library" to "Library"
  • Access control: Changed from "public"/"private" to "Public"/"Private"
  • Module structure: Changed from {"name": ..., "def": ...} to array-based [modulePath, accessControlled]
  • Type tags: Changed to capitalized forms (e.g., "variable" → "Variable")

Format Version 2 → 3

  • Value expression tags: Changed from lowercase to capitalized (e.g., "apply" → "Apply")
  • Pattern tags: Changed from lowercase with underscores to capitalized (e.g., "as_pattern" → "AsPattern")
  • Literal tags: Changed from lowercase with underscores to capitalized (e.g., "bool_literal" → "BoolLiteral")

Using the Schemas

The JSON schemas can be used for:

  1. Validation: Validate Morphir IR JSON files against the appropriate version schema
  2. Documentation: Understand the structure and constraints of the IR format
  3. Code Generation: Generate parsers, serializers, and type definitions for various languages
  4. Tooling: Build editors, linters, and other tools that work with Morphir IR

Example validation using a JSON schema validator:

# Using Python jsonschema (recommended for YAML schemas)
pip install jsonschema pyyaml
python -c "import json, yaml, jsonschema; \
  schema = yaml.safe_load(open('docs/content/spec/schemas/morphir-ir-v3.yaml')); \
  data = json.load(open('morphir-ir.json')); \
  jsonschema.validate(data, schema); \
  print('✓ Valid Morphir IR')"

# Using ajv-cli (Node.js) - requires converting YAML to JSON first
npm install -g ajv-cli
python -c "import yaml, json; \
  json.dump(yaml.safe_load(open('docs/content/spec/schemas/morphir-ir-v3.yaml')), \
  open('morphir-ir-v3.json', 'w'))"
ajv validate -s morphir-ir-v3.json -d morphir-ir.json

Schema Location

All schemas are located in the docs/content/spec/schemas/ directory of the Morphir .NET repository:

  • docs/content/spec/schemas/morphir-ir-v1.yaml
  • docs/content/spec/schemas/morphir-ir-v2.yaml
  • docs/content/spec/schemas/morphir-ir-v3.yaml

Conclusion

The Morphir IR provides a comprehensive, type-safe representation of functional business logic. Its design enables:

  • Portability: Same logic can target multiple platforms
  • Analysis: Logic can be analyzed for correctness and properties
  • Transformation: Logic can be optimized and adapted
  • Tooling: Rich development tools can be built on a standard format
  • Interoperability: Different languages can share logic via IR

This specification defines the structure and semantics necessary for building a robust ecosystem of Morphir tools and ensuring consistent interpretation across implementations. The accompanying JSON schemas provide formal, machine-readable definitions that can be used for validation, code generation, and tooling support.

4.2 - JSON Schemas

JSON schema definitions for Morphir IR format versions

Morphir IR JSON Schemas

This directory contains formal JSON schema specifications for all supported format versions of the Morphir IR (Intermediate Representation).

Schema Files

  • morphir-ir-v3.yaml: Current format version (v3)
  • morphir-ir-v2.yaml: Format version 2
  • morphir-ir-v1.yaml: Format version 1

Format Version Differences

Version 1 → Version 2

Tag Capitalization:

  • Distribution: "library" → "Library"
  • Access control: "public"/"private" → "Public"/"Private"
  • Type tags: "variable" → "Variable", "reference" → "Reference", etc.

Structure Changes:

  • Modules changed from {"name": ..., "def": ...} objects to [modulePath, accessControlled] arrays

Version 2 → Version 3

Tag Capitalization:

  • Value expression tags: "apply" → "Apply", "lambda" → "Lambda", etc.
  • Pattern tags: "as_pattern" → "AsPattern", "wildcard_pattern" → "WildcardPattern", etc.
  • Literal tags: "bool_literal" → "BoolLiteral", "string_literal" → "StringLiteral", etc.

Usage

Validation

The schemas can be used to validate Morphir IR JSON files. Note that due to the complexity and recursive nature of these schemas, validation can be slow with some validators.

Using Python jsonschema

pip install jsonschema pyyaml

python3 << 'EOF'
import json
import yaml
from jsonschema import validate

# Load schema
with open('morphir-ir-v3.yaml', 'r') as f:
    schema = yaml.safe_load(f)

# Load Morphir IR JSON
with open('morphir-ir.json', 'r') as f:
    data = json.load(f)

# Validate
validate(instance=data, schema=schema)
print("✓ Valid Morphir IR")
EOF

Using Node.js ajv

npm install -g ajv-cli ajv-formats

# Convert YAML to JSON first
python3 -c "import yaml, json; \
  json.dump(yaml.safe_load(open('morphir-ir-v3.yaml')), open('morphir-ir-v3.json', 'w'))"

# Validate
ajv validate -s morphir-ir-v3.json -d morphir-ir.json

Quick Structural Check

For a quick check without full validation, you can verify basic structure:

import json

def check_morphir_ir(filepath):
    with open(filepath) as f:
        data = json.load(f)
    
    # Check format version
    version = data.get('formatVersion')
    assert version in [1, 2, 3], f"Unknown format version: {version}"
    
    # Check distribution structure
    dist = data['distribution']
    assert isinstance(dist, list) and len(dist) == 4
    assert dist[0] in ["library", "Library"], f"Unknown distribution type: {dist[0]}"
    
    # Check package definition
    pkg_def = dist[3]
    assert 'modules' in pkg_def
    
    print(f"✓ Basic structure valid: Format v{version}, {len(pkg_def['modules'])} modules")

check_morphir_ir('morphir-ir.json')

Integration with Tools

These schemas can be used to:

  1. Generate Code: Create type definitions and parsers for various programming languages
  2. IDE Support: Provide autocomplete and validation in JSON editors
  3. Testing: Validate generated IR in test suites
  4. Documentation: Generate human-readable documentation from schema definitions

Schema Format

The schemas are written in YAML format for better readability and include:

  • Comprehensive inline documentation
  • Type constraints and patterns
  • Required vs. optional fields
  • Recursive type definitions
  • Enum values for tagged unions

Contributing

When updating the IR format:

  1. Update the appropriate schema file(s) to match the upstream schemas from the main Morphir repository
  2. Update the format version handling in the .NET codec implementation if needed
  3. Add migration logic in the codec files if needed
  4. Update this README with the changes
  5. Test the schema against example IR files

References

4.2.1 - Schema Version 3

Morphir IR JSON Schema for format version 3 (Current)

Morphir IR Schema - Version 3

Format version 3 is the current version of the Morphir IR format. It uses capitalized tags throughout for consistency and clarity.

Overview

Version 3 of the Morphir IR format standardizes on capitalized tags for all constructs. This provides a consistent naming convention across the entire IR structure.

Key Characteristics

Tag Capitalization

All tags in version 3 are capitalized:

  • Distribution: "Library"
  • Access Control: "Public" and "Private"
  • Type Tags: "Variable", "Reference", "Tuple", "Record", etc.
  • Value Tags: "Apply", "Lambda", "LetDefinition", etc.
  • Pattern Tags: "AsPattern", "WildcardPattern", "ConstructorPattern", etc.
  • Literal Tags: "BoolLiteral", "StringLiteral", "WholeNumberLiteral", etc.

Core Concepts

Naming System

The Morphir IR uses a sophisticated naming system independent of any specific naming convention.

Name

A Name represents a human-readable identifier made up of one or more words.

  • Structure: Array of lowercase word strings
  • Purpose: Atomic unit for all identifiers
  • Example: ["value", "in", "u", "s", "d"] renders as valueInUSD or value_in_USD
Name:
  type: array
  items:
    type: string
    pattern: "^[a-z][a-z0-9]*$"
  minItems: 1

Path

A Path represents a hierarchical location in the IR structure.

  • Structure: List of Names
  • Purpose: Identifies packages and modules
  • Example: [["morphir"], ["s", "d", "k"], ["string"]] for the String module
Path:
  type: array
  items:
    $ref: "#/definitions/Name"
  minItems: 1

Fully-Qualified Name (FQName)

Provides globally unique identifiers for types and values.

  • Structure: [packagePath, modulePath, localName]
  • Purpose: Unambiguous references across package boundaries
FQName:
  type: array
  minItems: 3
  maxItems: 3
  items:
    - $ref: "#/definitions/PackageName"
    - $ref: "#/definitions/ModuleName"
    - $ref: "#/definitions/Name"

Access Control

AccessControlled

Manages visibility of types and values.

  • Structure: {access, value}
  • Access levels: "Public" (visible externally) or "Private" (package-only)
  • Purpose: Controls API exposure
AccessControlled:
  type: object
  required: ["access", "value"]
  properties:
    access:
      type: string
      enum: ["Public", "Private"]
    value:
      description: "The value being access controlled."

Distribution and Package Structure

Distribution

A Distribution represents a complete, self-contained package with all dependencies.

  • Current type: Library (only supported distribution type)
  • Structure: ["Library", packageName, dependencies, packageDefinition]
  • Purpose: Output of compilation process, ready for execution or transformation
distribution:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - type: string
      const: "Library"
    - $ref: "#/definitions/PackageName"
    - $ref: "#/definitions/Dependencies"
    - $ref: "#/definitions/PackageDefinition"

Package Definition

Complete implementation of a package with all details.

  • Contains: All modules (public and private)
  • Includes: Type signatures and implementations
  • Purpose: Full package representation for processing

Package Specification

Public interface of a package.

  • Contains: Only publicly exposed modules
  • Includes: Only type signatures, no implementations
  • Purpose: Dependency interface

Module Structure

Module Definition

Complete implementation of a module.

  • Contains: All types and values (public and private) with implementations
  • Structure: Dictionary of type names to AccessControlled type definitions, and value names to AccessControlled value definitions
  • Purpose: Complete module implementation

Module Specification

Public interface of a module.

  • Contains: Only publicly exposed types and values
  • Includes: Type signatures only, no implementations
  • Purpose: Module’s public API

Type System

The type system is based on functional programming principles, supporting:

Type Expressions

Variable

Represents a type variable (generic parameter).

  • Structure: ["Variable", attributes, name]
  • Example: The a in List a
  • Purpose: Enables polymorphic types
VariableType:
  type: array
  minItems: 3
  maxItems: 3
  items:
    - const: "Variable"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/Name"

Reference

Reference to another type or type alias.

  • Structure: ["Reference", attributes, fqName, typeArgs]
  • Examples: String, List Int, Maybe a
  • Purpose: References built-in types, custom types, or type aliases
ReferenceType:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - const: "Reference"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/FQName"
    - type: array
      items:
        $ref: "#/definitions/Type"

Tuple

Composition of multiple types in fixed order.

  • Structure: ["Tuple", attributes, elementTypes]
  • Example: (Int, String, Bool)
  • Purpose: Product types with positional access

Record

Composition of named fields with types.

  • Structure: ["Record", attributes, fields]
  • Example: {firstName: String, age: Int}
  • Purpose: Product types with named field access
  • Note: All fields are required

Function

Function type representation.

  • Structure: ["Function", attributes, argType, returnType]
  • Example: Int -> String
  • Purpose: Represents function and lambda types
  • Note: Multi-argument functions use currying (nested Function types)

Type Specifications

TypeAliasSpecification

An alias for another type.

  • Structure: ["TypeAliasSpecification", typeParams, aliasedType]
  • Example: type alias UserId = String
  • Purpose: Meaningful name for type expression

CustomTypeSpecification

Tagged union type (sum type).

  • Structure: ["CustomTypeSpecification", typeParams, constructors]
  • Example: type Result e a = Ok a | Err e
  • Purpose: Choice between multiple alternatives

OpaqueTypeSpecification

Type with unknown structure.

  • Structure: ["OpaqueTypeSpecification", typeParams]
  • Characteristics: Structure hidden, no automatic serialization
  • Purpose: Encapsulates implementation details

Value System

All data and logic in Morphir are represented as value expressions.

Value Expressions

Literal

Literal constant value.

  • Structure: ["Literal", attributes, literal]
  • Types: BoolLiteral, CharLiteral, StringLiteral, WholeNumberLiteral, FloatLiteral, DecimalLiteral
  • Purpose: Represents constant data

Variable

Reference to a variable in scope.

  • Structure: ["Variable", attributes, name]
  • Example: References to function parameters or let-bound variables
  • Purpose: Accesses values bound in current scope

Reference

Reference to a defined value (function or constant).

  • Structure: ["Reference", attributes, fqName]
  • Example: Morphir.SDK.List.map, Basics.add
  • Purpose: Invokes or references defined functions

Apply

Function application.

  • Structure: ["Apply", attributes, function, argument]
  • Example: add 1 2 (nested Apply nodes for currying)
  • Purpose: Invokes functions with arguments

Lambda

Anonymous function.

  • Structure: ["Lambda", attributes, argumentPattern, body]
  • Example: \x -> x + 1
  • Purpose: Creates inline functions

LetDefinition

Let binding introducing a single value.

  • Structure: ["LetDefinition", attributes, bindingName, definition, inExpr]
  • Example: let x = 5 in x + x
  • Purpose: Introduces local bindings

IfThenElse

Conditional expression.

  • Structure: ["IfThenElse", attributes, condition, thenBranch, elseBranch]
  • Example: if x > 0 then "positive" else "non-positive"
  • Purpose: Conditional logic

PatternMatch

Pattern matching with multiple cases.

  • Structure: ["PatternMatch", attributes, valueToMatch, cases]
  • Example: case maybeValue of Just x -> x; Nothing -> 0
  • Purpose: Conditional logic based on structure

Patterns

Used for destructuring and filtering values.

WildcardPattern

Matches any value without binding.

  • Structure: ["WildcardPattern", attributes]
  • Syntax: _
  • Purpose: Ignores a value

AsPattern

Binds a name to a matched value.

  • Structure: ["AsPattern", attributes, nestedPattern, variableName]
  • Special case: Simple variable binding uses AsPattern with WildcardPattern
  • Purpose: Captures matched values

ConstructorPattern

Matches specific type constructor and arguments.

  • Structure: ["ConstructorPattern", attributes, fqName, argPatterns]
  • Example: Just x matches Just with pattern x
  • Purpose: Destructures and filters tagged unions

Literals

BoolLiteral

Boolean literal.

  • Structure: ["BoolLiteral", boolean]
  • Values: true or false
BoolLiteral:
  type: array
  minItems: 2
  maxItems: 2
  items:
    - const: "BoolLiteral"
    - type: boolean

StringLiteral

Text string literal.

  • Structure: ["StringLiteral", string]
  • Example: "hello"
StringLiteral:
  type: array
  minItems: 2
  maxItems: 2
  items:
    - const: "StringLiteral"
    - type: string

WholeNumberLiteral

Integer literal.

  • Structure: ["WholeNumberLiteral", integer]
  • Example: 42, -17

Version 3 is the recommended format for new Morphir IR files. It provides:

  • Consistency: All tags follow the same capitalization convention
  • Clarity: Capitalized tags are easier to distinguish in JSON
  • Future-proof: This format will be maintained going forward

Full Schema

For the complete schema definition, see the full schema page.

References

4.2.1.1 - What's New in Version 3

Changes and improvements in Morphir IR schema version 3

What’s New in Version 3

Version 3 of the Morphir IR schema introduces consistent capitalization across all tags, providing a uniform and predictable format.

Key Changes from Version 2

Consistent Capitalization

The primary change in version 3 is the complete capitalization of all tags throughout the schema:

Value Expression Tags

All value expression tags are now capitalized:

  • "apply" → "Apply"
  • "lambda" → "Lambda"
  • "let_definition" → "LetDefinition"
  • "if_then_else" → "IfThenElse"
  • "pattern_match" → "PatternMatch"
  • "literal" → "Literal"
  • "variable" → "Variable"
  • "reference" → "Reference"
  • "constructor" → "Constructor"
  • "tuple" → "Tuple"
  • "list" → "List"
  • "record" → "Record"
  • "field" → "Field"
  • "field_function" → "FieldFunction"
  • "let_recursion" → "LetRecursion"
  • "destructure" → "Destructure"
  • "update_record" → "UpdateRecord"
  • "unit" → "Unit"

Pattern Tags

All pattern tags are now capitalized:

  • "wildcard_pattern" → "WildcardPattern"
  • "as_pattern" → "AsPattern"
  • "tuple_pattern" → "TuplePattern"
  • "constructor_pattern" → "ConstructorPattern"
  • "empty_list_pattern" → "EmptyListPattern"
  • "head_tail_pattern" → "HeadTailPattern"
  • "literal_pattern" → "LiteralPattern"
  • "unit_pattern" → "UnitPattern"

Literal Tags

All literal tags are now capitalized:

  • "bool_literal" → "BoolLiteral"
  • "char_literal" → "CharLiteral"
  • "string_literal" → "StringLiteral"
  • "whole_number_literal" → "WholeNumberLiteral"
  • "float_literal" → "FloatLiteral"
  • "decimal_literal" → "DecimalLiteral"

Benefits

Consistency

Version 3 provides a single, uniform naming convention across the entire IR structure. This makes the schema:

  • Easier to remember: One rule applies everywhere
  • More predictable: All tags follow PascalCase capitalization
  • Cleaner to work with: No need to remember which tags use underscores or lowercase

Better Tooling Support

The consistent capitalization improves:

  • Code generation: Automated tools can rely on uniform naming
  • Serialization/Deserialization: Simplified mapping to programming language types
  • Validation: Easier to write validation rules and tests

Migration from Version 2

Migrating from version 2 to version 3 requires updating all lowercase and underscore-separated tags:

  1. Capitalize all value tags
  2. Capitalize all pattern tags
  3. Capitalize all literal tags
  4. Remove underscores and use PascalCase

Recommendation

Version 3 is the current and recommended format for all new Morphir IR files. It provides the best balance of consistency, clarity, and tooling support.

See Also

4.2.1.2 - Full Schema

Complete Morphir IR JSON Schema for format version 3

Morphir IR Schema Version 3 - Complete Schema

This page contains the complete JSON schema definition for Morphir IR format version 3 (current version).

Download

You can download the schema file directly: morphir-ir-v3.yaml

Usage

This schema can be used to validate Morphir IR JSON files in format version 3:

# Using Python jsonschema (recommended for YAML schemas)
pip install jsonschema pyyaml
python -c "import json, yaml, jsonschema; \
  schema = yaml.safe_load(open('morphir-ir-v3.yaml')); \
  data = json.load(open('your-morphir-ir.json')); \
  jsonschema.validate(data, schema); \
  print('✓ Valid Morphir IR')"

References


Appendix: Complete Schema Definition

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
# JSON Schema for Morphir IR Format Version 3
# This schema defines the structure of a Morphir IR distribution in version 3 format.
# A distribution is the output of the Morphir compilation process (e.g., morphir-elm make).

$schema: "http://json-schema.org/draft-07/schema#"
$id: "https://finos.github.io/morphir/schemas/morphir-ir-v3.yaml"
title: "Morphir IR Distribution"
description: |
  A Morphir IR distribution represents a complete, self-contained package of business logic
  with all its dependencies. It captures the semantics of functional programs in a
  language-independent, platform-agnostic format.

type: object
required:
  - formatVersion
  - distribution
properties:
  formatVersion:
    type: integer
    const: 3
    description: "The version of the IR format. Must be 3 for this schema."
  
  distribution:
    description: "The distribution data, currently only Library distributions are supported."
    type: array
    minItems: 4
    maxItems: 4
    items:
      - type: string
        const: "Library"
        description: "The type of distribution. Currently only Library is supported."
      - $ref: "#/definitions/PackageName"
      - $ref: "#/definitions/Dependencies"
      - $ref: "#/definitions/PackageDefinition"

definitions:
  # ===== Basic Building Blocks =====
  
  Name:
    type: array
    items:
      type: string
      pattern: "^[a-z][a-z0-9]*$"
    minItems: 1
    description: |
      A Name is a list of lowercase words that represents a human-readable identifier.
      Example: ["value", "in", "u", "s", "d"] can be rendered as valueInUSD or value_in_USD.
  
  Path:
    type: array
    items:
      $ref: "#/definitions/Name"
    minItems: 1
    description: |
      A Path is a list of Names representing a hierarchical location in the IR structure.
      Used for package names and module names.
  
  PackageName:
    $ref: "#/definitions/Path"
    description: "Globally unique identifier for a package."
  
  ModuleName:
    $ref: "#/definitions/Path"
    description: "Unique identifier for a module within a package."
  
  FQName:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - $ref: "#/definitions/PackageName"
      - $ref: "#/definitions/ModuleName"
      - $ref: "#/definitions/Name"
    description: |
      Fully-Qualified Name that provides a globally unique identifier for any type or value.
      Consists of [packagePath, modulePath, localName].
  
  # ===== Attributes =====
  
  Attributes:
    type: object
    description: |
      Attributes can be attached to various nodes in the IR for extensibility.
      When no additional information is needed, an empty object {} is used.
  
  # ===== Access Control =====
  
  AccessControlled:
    type: object
    required: ["access", "value"]
    properties:
      access:
        type: string
        enum: ["Public", "Private"]
        description: "Controls visibility of types and values."
      value:
        description: "The value being access controlled."
    description: "Wrapper that manages visibility of types and values."
  
  # Note: Documented is not a separate schema definition because it's encoded conditionally.
  # When documentation exists, the JSON has both "doc" and "value" fields.
  # When documentation is absent, the JSON contains only the documented element directly (no wrapper).
  # This is handled inline in the definitions that use Documented.
  
  # ===== Distribution Structure =====
  
  Dependencies:
    type: array
    items:
      type: array
      minItems: 2
      maxItems: 2
      items:
        - $ref: "#/definitions/PackageName"
        - $ref: "#/definitions/PackageSpecification"
    description: "Dictionary of package dependencies, contains only type signatures."
  
  PackageDefinition:
    type: object
    required: ["modules"]
    properties:
      modules:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/ModuleName"
            - allOf:
                - $ref: "#/definitions/AccessControlled"
                - properties:
                    value:
                      $ref: "#/definitions/ModuleDefinition"
        description: "All modules in the package (public and private)."
    description: "Complete implementation of a package with all details."
  
  PackageSpecification:
    type: object
    required: ["modules"]
    properties:
      modules:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/ModuleName"
            - $ref: "#/definitions/ModuleSpecification"
        description: "Public modules only."
    description: "Public interface of a package, contains only type signatures."
  
  # ===== Module Structure =====
  
  ModuleDefinition:
    type: object
    required: ["types", "values"]
    properties:
      types:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - allOf:
                - $ref: "#/definitions/AccessControlled"
                - properties:
                    value:
                      # Documented wrapper: can have "doc" and "value", or just the type definition directly
                      oneOf:
                        - type: object
                          required: ["doc", "value"]
                          properties:
                            doc:
                              type: string
                            value:
                              $ref: "#/definitions/TypeDefinition"
                        - $ref: "#/definitions/TypeDefinition"
        description: "All type definitions (public and private)."
      values:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - allOf:
                - $ref: "#/definitions/AccessControlled"
                - properties:
                    value:
                      # Documented wrapper: can have "doc" and "value", or just the value definition directly
                      oneOf:
                        - type: object
                          required: ["doc", "value"]
                          properties:
                            doc:
                              type: string
                            value:
                              $ref: "#/definitions/ValueDefinition"
                        - $ref: "#/definitions/ValueDefinition"
        description: "All value definitions (public and private)."
      doc:
        type: string
        description: "Optional documentation for the module."
    description: "Complete implementation of a module."
  
  ModuleSpecification:
    type: object
    required: ["types", "values"]
    properties:
      types:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - oneOf:
                - type: object
                  required: ["doc", "value"]
                  properties:
                    doc:
                      type: string
                    value:
                      $ref: "#/definitions/TypeSpecification"
                - $ref: "#/definitions/TypeSpecification"
        description: "Public type specifications only."
      values:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - oneOf:
                - type: object
                  required: ["doc", "value"]
                  properties:
                    doc:
                      type: string
                    value:
                      $ref: "#/definitions/ValueSpecification"
                - $ref: "#/definitions/ValueSpecification"
        description: "Public value specifications only."
      doc:
        type: string
        description: "Optional documentation for the module."
    description: "Public interface of a module."
  
  # ===== Type System =====
  
  Type:
    description: |
      A Type is a recursive tree structure representing type expressions.
      Each type can be one of: Variable, Reference, Tuple, Record, ExtensibleRecord, Function, or Unit.
    oneOf:
      - $ref: "#/definitions/VariableType"
      - $ref: "#/definitions/ReferenceType"
      - $ref: "#/definitions/TupleType"
      - $ref: "#/definitions/RecordType"
      - $ref: "#/definitions/ExtensibleRecordType"
      - $ref: "#/definitions/FunctionType"
      - $ref: "#/definitions/UnitType"
  
  VariableType:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Variable"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
    description: "Represents a type variable (generic parameter)."
  
  ReferenceType:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "Reference"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
      - type: array
        items:
          $ref: "#/definitions/Type"
        description: "Type arguments for generic types."
    description: "Reference to another type or type alias."
  
  TupleType:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Tuple"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Type"
        description: "Element types in order."
    description: "A composition of multiple types in a fixed order (product type)."
  
  RecordType:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Record"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Field"
        description: "List of field definitions."
    description: "A composition of named fields with their types."
  
  ExtensibleRecordType:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "ExtensibleRecord"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
      - type: array
        items:
          $ref: "#/definitions/Field"
        description: "Known fields."
    description: "A record type that can be extended with additional fields."
  
  FunctionType:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "Function"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Type"
      - $ref: "#/definitions/Type"
    description: |
      Represents a function type. Multi-argument functions are represented via currying.
      Items: [tag, attributes, argumentType, returnType]
  
  UnitType:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "Unit"
      - $ref: "#/definitions/Attributes"
    description: "The type with exactly one value (similar to void in some languages)."
  
  Field:
    type: object
    required: ["name", "tpe"]
    properties:
      name:
        $ref: "#/definitions/Name"
        description: "Field name."
      tpe:
        $ref: "#/definitions/Type"
        description: "Field type."
    description: "A field in a record type."
  
  # ===== Type Specifications =====
  
  TypeSpecification:
    description: "Defines the interface of a type without implementation details."
    oneOf:
      - $ref: "#/definitions/TypeAliasSpecification"
      - $ref: "#/definitions/OpaqueTypeSpecification"
      - $ref: "#/definitions/CustomTypeSpecification"
      - $ref: "#/definitions/DerivedTypeSpecification"
  
  TypeAliasSpecification:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "TypeAliasSpecification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - $ref: "#/definitions/Type"
    description: "An alias for another type."
  
  OpaqueTypeSpecification:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "OpaqueTypeSpecification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
    description: |
      A type with unknown structure. The implementation is hidden from consumers.
  
  CustomTypeSpecification:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "CustomTypeSpecification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - $ref: "#/definitions/Constructors"
    description: "A tagged union type (sum type)."
  
  DerivedTypeSpecification:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "DerivedTypeSpecification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - type: object
        required: ["baseType", "fromBaseType", "toBaseType"]
        properties:
          baseType:
            $ref: "#/definitions/Type"
            description: "The type used for serialization."
          fromBaseType:
            $ref: "#/definitions/FQName"
            description: "Function to convert from base type."
          toBaseType:
            $ref: "#/definitions/FQName"
            description: "Function to convert to base type."
        description: "Details for derived type."
    description: |
      A type with platform-specific representation but known serialization.
  
  # ===== Type Definitions =====
  
  TypeDefinition:
    description: "Provides the complete implementation of a type."
    oneOf:
      - $ref: "#/definitions/TypeAliasDefinition"
      - $ref: "#/definitions/CustomTypeDefinition"
  
  TypeAliasDefinition:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "TypeAliasDefinition"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - $ref: "#/definitions/Type"
    description: "Complete definition of a type alias."
  
  CustomTypeDefinition:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "CustomTypeDefinition"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - allOf:
          - $ref: "#/definitions/AccessControlled"
          - properties:
              value:
                $ref: "#/definitions/Constructors"
    description: |
      Complete definition of a custom type. If constructors are Private, 
      the specification becomes OpaqueTypeSpecification.
  
  Constructors:
    type: array
    items:
      type: array
      minItems: 2
      maxItems: 2
      items:
        - $ref: "#/definitions/Name"
        - type: array
          items:
            type: array
            minItems: 2
            maxItems: 2
            items:
              - $ref: "#/definitions/Name"
              - $ref: "#/definitions/Type"
          description: "Constructor arguments as (name, type) pairs."
    description: "Dictionary of constructor names to their typed arguments."
  
  # ===== Value System =====
  
  Value:
    description: |
      A Value is a recursive tree structure representing computations.
      All data and logic in Morphir are represented as value expressions.
    oneOf:
      - $ref: "#/definitions/LiteralValue"
      - $ref: "#/definitions/ConstructorValue"
      - $ref: "#/definitions/TupleValue"
      - $ref: "#/definitions/ListValue"
      - $ref: "#/definitions/RecordValue"
      - $ref: "#/definitions/VariableValue"
      - $ref: "#/definitions/ReferenceValue"
      - $ref: "#/definitions/FieldValue"
      - $ref: "#/definitions/FieldFunctionValue"
      - $ref: "#/definitions/ApplyValue"
      - $ref: "#/definitions/LambdaValue"
      - $ref: "#/definitions/LetDefinitionValue"
      - $ref: "#/definitions/LetRecursionValue"
      - $ref: "#/definitions/DestructureValue"
      - $ref: "#/definitions/IfThenElseValue"
      - $ref: "#/definitions/PatternMatchValue"
      - $ref: "#/definitions/UpdateRecordValue"
      - $ref: "#/definitions/UnitValue"
  
  LiteralValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Literal"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Literal"
    description: "A literal constant value."
  
  ConstructorValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Constructor"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
    description: "Reference to a custom type constructor."
  
  TupleValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Tuple"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Value"
        description: "Element values in order."
    description: "A tuple value with multiple elements."
  
  ListValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "List"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Value"
        description: "List elements."
    description: "A list of values."
  
  RecordValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Record"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Value"
        description: "Dictionary mapping field names to values."
    description: "A record value with named fields."
  
  VariableValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Variable"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
    description: "Reference to a variable in scope."
  
  ReferenceValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Reference"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
    description: "Reference to a defined value (function or constant)."
  
  FieldValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "Field"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Name"
    description: "Field access on a record. Items: [tag, attributes, recordExpr, fieldName]"
  
  FieldFunctionValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "FieldFunction"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
    description: "A function that extracts a field (e.g., .firstName)."
  
  ApplyValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "Apply"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
    description: |
      Function application. Items: [tag, attributes, function, argument].
      Multi-argument calls are represented via currying (nested Apply nodes).
  
  LambdaValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "Lambda"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Value"
    description: |
      Anonymous function (lambda abstraction).
      Items: [tag, attributes, argumentPattern, body]
  
  LetDefinitionValue:
    type: array
    minItems: 5
    maxItems: 5
    items:
      - const: "LetDefinition"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
      - $ref: "#/definitions/ValueDefinition"
      - $ref: "#/definitions/Value"
    description: |
      A let binding introducing a single value.
      Items: [tag, attributes, bindingName, definition, inExpr]
  
  LetRecursionValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "LetRecursion"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/ValueDefinition"
        description: "Multiple bindings that can reference each other."
      - $ref: "#/definitions/Value"
    description: "Mutually recursive let bindings."
  
  DestructureValue:
    type: array
    minItems: 5
    maxItems: 5
    items:
      - const: "Destructure"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
    description: |
      Pattern-based destructuring.
      Items: [tag, attributes, pattern, valueToDestructure, inExpr]
  
  IfThenElseValue:
    type: array
    minItems: 5
    maxItems: 5
    items:
      - const: "IfThenElse"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
    description: |
      Conditional expression.
      Items: [tag, attributes, condition, thenBranch, elseBranch]
  
  PatternMatchValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "PatternMatch"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Pattern"
            - $ref: "#/definitions/Value"
        description: "List of pattern-branch pairs."
    description: "Pattern matching with multiple cases."
  
  UpdateRecordValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "UpdateRecord"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Value"
        description: "Fields to update with new values."
    description: |
      Record update expression (immutable copy-on-update).
      Items: [tag, attributes, recordToUpdate, fieldsToUpdate]
  
  UnitValue:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "Unit"
      - $ref: "#/definitions/Attributes"
    description: "The unit value (the single value of the Unit type)."
  
  # ===== Literals =====
  
  Literal:
    description: "Represents literal constant values."
    oneOf:
      - $ref: "#/definitions/BoolLiteral"
      - $ref: "#/definitions/CharLiteral"
      - $ref: "#/definitions/StringLiteral"
      - $ref: "#/definitions/WholeNumberLiteral"
      - $ref: "#/definitions/FloatLiteral"
      - $ref: "#/definitions/DecimalLiteral"
  
  BoolLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "BoolLiteral"
      - type: boolean
    description: "Boolean literal (true or false)."
  
  CharLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "CharLiteral"
      - type: string
        minLength: 1
        maxLength: 1
    description: "Single character literal."
  
  StringLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "StringLiteral"
      - type: string
    description: "Text string literal."
  
  WholeNumberLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "WholeNumberLiteral"
      - type: integer
    description: "Integer literal."
  
  FloatLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "FloatLiteral"
      - type: number
    description: "Floating-point number literal."
  
  DecimalLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "DecimalLiteral"
      - type: string
        pattern: "^-?[0-9]+(\\.[0-9]+)?$"
    description: "Arbitrary-precision decimal literal (stored as string)."
  
  # ===== Patterns =====
  
  Pattern:
    description: |
      Patterns are used for destructuring and filtering values.
      They appear in lambda, let destructure, and pattern match expressions.
    oneOf:
      - $ref: "#/definitions/WildcardPattern"
      - $ref: "#/definitions/AsPattern"
      - $ref: "#/definitions/TuplePattern"
      - $ref: "#/definitions/ConstructorPattern"
      - $ref: "#/definitions/EmptyListPattern"
      - $ref: "#/definitions/HeadTailPattern"
      - $ref: "#/definitions/LiteralPattern"
      - $ref: "#/definitions/UnitPattern"
  
  WildcardPattern:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "WildcardPattern"
      - $ref: "#/definitions/Attributes"
    description: "Matches any value without binding (the _ pattern)."
  
  AsPattern:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "AsPattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Name"
    description: |
      Binds a name to a value matched by a nested pattern.
      Simple variable binding is AsPattern with WildcardPattern nested.
      Items: [tag, attributes, nestedPattern, variableName]
  
  TuplePattern:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "TuplePattern"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Pattern"
        description: "Patterns for each tuple element."
    description: "Matches a tuple by matching each element."
  
  ConstructorPattern:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "ConstructorPattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
      - type: array
        items:
          $ref: "#/definitions/Pattern"
        description: "Patterns for constructor arguments."
    description: "Matches a specific type constructor and its arguments."
  
  EmptyListPattern:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "EmptyListPattern"
      - $ref: "#/definitions/Attributes"
    description: "Matches an empty list (the [] pattern)."
  
  HeadTailPattern:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "HeadTailPattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Pattern"
    description: |
      Matches a non-empty list by head and tail (the x :: xs pattern).
      Items: [tag, attributes, headPattern, tailPattern]
  
  LiteralPattern:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "LiteralPattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Literal"
    description: "Matches an exact literal value."
  
  UnitPattern:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "UnitPattern"
      - $ref: "#/definitions/Attributes"
    description: "Matches the unit value."
  
  # ===== Value Specifications and Definitions =====
  
  ValueSpecification:
    type: object
    required: ["inputs", "output"]
    properties:
      inputs:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Type"
        description: "Function parameters as (name, type) pairs."
      output:
        $ref: "#/definitions/Type"
        description: "The return type."
    description: |
      The type signature of a value or function.
      Contains only type information, no implementation.
  
  ValueDefinition:
    type: object
    required: ["inputTypes", "outputType", "body"]
    properties:
      inputTypes:
        type: array
        items:
          type: array
          minItems: 3
          maxItems: 3
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Attributes"
            - $ref: "#/definitions/Type"
        description: "Function parameters as (name, attributes, type) tuples."
      outputType:
        $ref: "#/definitions/Type"
        description: "The return type."
      body:
        $ref: "#/definitions/Value"
        description: "The value expression implementing the logic."
    description: |
      The complete implementation of a value or function.
      Contains both type information and implementation.

4.2.2 - Schema Version 2

Morphir IR JSON Schema for format version 2

Morphir IR Schema - Version 2

Format version 2 introduced capitalized tags for distribution, access control, and types, while keeping value and pattern tags lowercase.

Overview

Version 2 of the Morphir IR format represents a transition between version 1 (all lowercase) and version 3 (all capitalized). It uses capitalized tags for distribution, access control, and types, but keeps value expressions and patterns in lowercase.

Key Characteristics

Tag Capitalization

Version 2 uses a mixed capitalization approach:

Capitalized:

  • Distribution: "Library" (capitalized)
  • Access Control: "Public" and "Private" (capitalized)
  • Type Tags: "Variable", "Reference", "Tuple", "Record", etc.

Lowercase:

  • Value Tags: "apply", "lambda", "let_definition", etc.
  • Pattern Tags: "as_pattern", "wildcard_pattern", etc.
  • Literal Tags: "bool_literal", "string_literal", etc.

Module Structure

Version 2 changed the module structure from objects to arrays:

modules:
  type: array
  items:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - $ref: "#/definitions/ModuleName"
      - allOf:
          - $ref: "#/definitions/AccessControlled"
          - properties:
              value:
                $ref: "#/definitions/ModuleDefinition"

This is a significant change from version 1’s {"name": ..., "def": ...} structure.

Core Concepts

Naming System

The Morphir IR uses a sophisticated naming system independent of any specific naming convention.

Name

A Name represents a human-readable identifier made up of one or more words.

  • Structure: Array of lowercase word strings
  • Purpose: Atomic unit for all identifiers
  • Example: ["value", "in", "u", "s", "d"] renders as valueInUSD or value_in_USD
Name:
  type: array
  items:
    type: string
    pattern: "^[a-z][a-z0-9]*$"
  minItems: 1

Path

A Path represents a hierarchical location in the IR structure.

  • Structure: List of Names
  • Purpose: Identifies packages and modules
  • Example: [["morphir"], ["s", "d", "k"], ["string"]] for the String module
Path:
  type: array
  items:
    $ref: "#/definitions/Name"
  minItems: 1

Fully-Qualified Name (FQName)

Provides globally unique identifiers for types and values.

  • Structure: [packagePath, modulePath, localName]
  • Purpose: Unambiguous references across package boundaries
FQName:
  type: array
  minItems: 3
  maxItems: 3
  items:
    - $ref: "#/definitions/PackageName"
    - $ref: "#/definitions/ModuleName"
    - $ref: "#/definitions/Name"

Access Control

AccessControlled

Manages visibility of types and values.

  • Structure: {access, value}
  • Access levels: "Public" (visible externally) or "Private" (package-only)
  • Purpose: Controls API exposure
  • Version 2 note: Capitalized access levels ("Public", "Private")
AccessControlled:
  type: object
  required: ["access", "value"]
  properties:
    access:
      type: string
      enum: ["Public", "Private"]
    value:
      description: "The value being access controlled."

Distribution and Package Structure

Distribution

A Distribution represents a complete, self-contained package with all dependencies.

  • Current type: Library (only supported distribution type)
  • Structure: ["Library", packageName, dependencies, packageDefinition]
  • Purpose: Output of compilation process, ready for execution or transformation
  • Version 2 note: Uses capitalized "Library" tag
distribution:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - type: string
      const: "Library"
    - $ref: "#/definitions/PackageName"
    - $ref: "#/definitions/Dependencies"
    - $ref: "#/definitions/PackageDefinition"

Package Definition

Complete implementation of a package with all details.

  • Contains: All modules (public and private)
  • Includes: Type signatures and implementations
  • Purpose: Full package representation for processing
  • Version 2 note: Modules stored as arrays of [name, accessControlledDefinition] pairs

Package Specification

Public interface of a package.

  • Contains: Only publicly exposed modules
  • Includes: Only type signatures, no implementations
  • Purpose: Dependency interface

Module Structure Details

Module Definition

Complete implementation of a module.

  • Contains: All types and values (public and private) with implementations
  • Structure: Dictionary of type names to AccessControlled type definitions, and value names to AccessControlled value definitions
  • Purpose: Complete module implementation

Module Specification

Public interface of a module.

  • Contains: Only publicly exposed types and values
  • Includes: Type signatures only, no implementations
  • Purpose: Module’s public API

Type System

The type system is based on functional programming principles, supporting:

Type Expressions

Version 2 note: Type tags are capitalized in version 2.

Variable

Represents a type variable (generic parameter).

  • Structure: ["Variable", attributes, name]
  • Example: The a in List a
  • Purpose: Enables polymorphic types
VariableType:
  type: array
  minItems: 3
  maxItems: 3
  items:
    - const: "Variable"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/Name"

Reference

Reference to another type or type alias.

  • Structure: ["Reference", attributes, fqName, typeArgs]
  • Examples: String, List Int, Maybe a
  • Purpose: References built-in types, custom types, or type aliases
ReferenceType:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - const: "Reference"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/FQName"
    - type: array
      items:
        $ref: "#/definitions/Type"

Tuple

Composition of multiple types in fixed order.

  • Structure: ["Tuple", attributes, elementTypes]
  • Example: (Int, String, Bool)
  • Purpose: Product types with positional access

Record

Composition of named fields with types.

  • Structure: ["Record", attributes, fields]
  • Example: {firstName: String, age: Int}
  • Purpose: Product types with named field access
  • Note: All fields are required

Function

Function type representation.

  • Structure: ["Function", attributes, argType, returnType]
  • Example: Int -> String
  • Purpose: Represents function and lambda types
  • Note: Multi-argument functions use currying (nested Function types)

Type Specifications

TypeAliasSpecification

An alias for another type.

  • Structure: ["TypeAliasSpecification", typeParams, aliasedType]
  • Example: type alias UserId = String
  • Purpose: Meaningful name for type expression

CustomTypeSpecification

Tagged union type (sum type).

  • Structure: ["CustomTypeSpecification", typeParams, constructors]
  • Example: type Result e a = Ok a | Err e
  • Purpose: Choice between multiple alternatives

OpaqueTypeSpecification

Type with unknown structure.

  • Structure: ["OpaqueTypeSpecification", typeParams]
  • Characteristics: Structure hidden, no automatic serialization
  • Purpose: Encapsulates implementation details

Value System

All data and logic in Morphir are represented as value expressions.

Version 2 note: Value tags are lowercase in version 2.

Value Expressions

literal

Literal constant value.

  • Structure: ["literal", attributes, literal]
  • Types: bool_literal, char_literal, string_literal, whole_number_literal, float_literal, decimal_literal
  • Purpose: Represents constant data

variable

Reference to a variable in scope.

  • Structure: ["variable", attributes, name]
  • Example: References to function parameters or let-bound variables
  • Purpose: Accesses values bound in current scope

reference

Reference to a defined value (function or constant).

  • Structure: ["reference", attributes, fqName]
  • Example: Morphir.SDK.List.map, Basics.add
  • Purpose: Invokes or references defined functions

apply

Function application.

  • Structure: ["apply", attributes, function, argument]
  • Example: add 1 2 (nested apply nodes for currying)
  • Purpose: Invokes functions with arguments
ApplyValue:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - const: "apply"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/Value"
    - $ref: "#/definitions/Value"

lambda

Anonymous function.

  • Structure: ["lambda", attributes, argumentPattern, body]
  • Example: \x -> x + 1
  • Purpose: Creates inline functions
LambdaValue:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - const: "lambda"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/Pattern"
    - $ref: "#/definitions/Value"

let_definition

Let binding introducing a single value.

  • Structure: ["let_definition", attributes, bindingName, definition, inExpr]
  • Example: let x = 5 in x + x
  • Purpose: Introduces local bindings

if_then_else

Conditional expression.

  • Structure: ["if_then_else", attributes, condition, thenBranch, elseBranch]
  • Example: if x > 0 then "positive" else "non-positive"
  • Purpose: Conditional logic

pattern_match

Pattern matching with multiple cases.

  • Structure: ["pattern_match", attributes, valueToMatch, cases]
  • Example: case maybeValue of Just x -> x; Nothing -> 0
  • Purpose: Conditional logic based on structure

Patterns

Used for destructuring and filtering values.

Version 2 note: Pattern tags are lowercase in version 2.

wildcard_pattern

Matches any value without binding.

  • Structure: ["wildcard_pattern", attributes]
  • Syntax: _
  • Purpose: Ignores a value

as_pattern

Binds a name to a matched value.

  • Structure: ["as_pattern", attributes, nestedPattern, variableName]
  • Special case: Simple variable binding uses as_pattern with wildcard_pattern
  • Purpose: Captures matched values

constructor_pattern

Matches specific type constructor and arguments.

  • Structure: ["constructor_pattern", attributes, fqName, argPatterns]
  • Example: Just x matches Just with pattern x
  • Purpose: Destructures and filters tagged unions

Literals

Version 2 note: Literal tags are lowercase in version 2.

bool_literal

Boolean literal.

  • Structure: ["bool_literal", boolean]
  • Values: true or false
BoolLiteral:
  type: array
  minItems: 2
  maxItems: 2
  items:
    - const: "bool_literal"
    - type: boolean

string_literal

Text string literal.

  • Structure: ["string_literal", string]
  • Example: "hello"
StringLiteral:
  type: array
  minItems: 2
  maxItems: 2
  items:
    - const: "string_literal"
    - type: string

whole_number_literal

Integer literal.

  • Structure: ["whole_number_literal", integer]
  • Example: 42, -17

Migration from Version 2

When migrating from version 2 to version 3:

  1. Capitalize value tags: "apply" → "Apply", "lambda" → "Lambda", etc.
  2. Capitalize pattern tags: "as_pattern" → "AsPattern", "wildcard_pattern" → "WildcardPattern", etc.
  3. Capitalize literal tags: "bool_literal" → "BoolLiteral", "string_literal" → "StringLiteral", etc.

Full Schema

For the complete schema definition, see the full schema page.

References

4.2.2.1 - What's New in Version 2

Changes and improvements in Morphir IR schema version 2

What’s New in Version 2

Version 2 of the Morphir IR schema introduces partial capitalization and a new module structure, representing a significant evolution from version 1.

Key Changes from Version 1

Partial Capitalization

Version 2 introduces capitalization for distribution, access control, and type-related tags:

Capitalized Tags

Distribution:

  • "library" → "Library"

Access Control:

  • "public" → "Public"
  • "private" → "Private"

Type Tags:

  • "variable" → "Variable"
  • "reference" → "Reference"
  • "tuple" → "Tuple"
  • "record" → "Record"
  • "extensible_record" → "ExtensibleRecord"
  • "function" → "Function"
  • "unit" → "Unit"

Type Specifications:

  • "type_alias_specification" → "TypeAliasSpecification"
  • "opaque_type_specification" → "OpaqueTypeSpecification"
  • "custom_type_specification" → "CustomTypeSpecification"
  • "derived_type_specification" → "DerivedTypeSpecification"

Type Definitions:

  • "type_alias_definition" → "TypeAliasDefinition"
  • "custom_type_definition" → "CustomTypeDefinition"

Unchanged (Lowercase) Tags

Value expressions remain lowercase:

  • "apply", "lambda", "let_definition", "if_then_else", etc.

Patterns remain lowercase:

  • "wildcard_pattern", "as_pattern", "constructor_pattern", etc.

Literals remain lowercase:

  • "bool_literal", "string_literal", "whole_number_literal", etc.

New Module Structure

Version 2 changes how modules are represented in packages:

Version 1 Structure

{
  "modules": [
    {
      "name": [["my"], ["module"]],
      "def": ["public", { ... }]
    }
  ]
}

Version 2 Structure

{
  "modules": [
    [
      [["my"], ["module"]],
      {
        "access": "Public",
        "value": { ... }
      }
    ]
  ]
}

Changes:

  • Modules are now represented as arrays instead of objects
  • Structure changed from {"name": ..., "def": ...} to [modulePath, accessControlled]
  • Access control uses the new AccessControlled wrapper with capitalized values

Access Control Wrapper

Version 2 introduces a structured AccessControlled wrapper:

AccessControlled:
  type: object
  required: ["access", "value"]
  properties:
    access:
      type: string
      enum: ["Public", "Private"]
    value:
      description: "The value being access controlled."

This provides a consistent way to manage visibility across types and values.

Benefits

Improved Clarity

  • Capitalized type tags stand out more clearly in JSON structures
  • Structured access control makes visibility explicit and consistent
  • Array-based module structure is more compact and follows the pattern used elsewhere in the IR

Better Type Safety

The structured AccessControlled wrapper provides:

  • Explicit access level declaration
  • Type-safe representation
  • Easier validation

Foundation for Version 3

Version 2 serves as a transition toward the fully capitalized format in version 3, making eventual migration easier.

Migration from Version 1

To migrate from version 1 to version 2:

  1. Capitalize distribution tag: "library" → "Library"
  2. Capitalize access control: "public" → "Public", "private" → "Private"
  3. Update module structure: Convert {"name": ..., "def": ...} to array format
  4. Capitalize all type tags: "variable" → "Variable", "reference" → "Reference", etc.
  5. Capitalize type specification and definition tags

Looking Forward

While version 2 introduces important improvements, version 3 completes the capitalization by extending it to value expressions, patterns, and literals. For new projects, consider using version 3 directly for maximum consistency.

See Also

4.2.2.2 - Full Schema

Complete Morphir IR JSON Schema for format version 2

Morphir IR Schema Version 2 - Complete Schema

This page contains the complete JSON schema definition for Morphir IR format version 2.

Download

You can download the schema file directly: morphir-ir-v2.yaml

Usage

This schema can be used to validate Morphir IR JSON files in format version 2:

# Using Python jsonschema (recommended for YAML schemas)
pip install jsonschema pyyaml
python -c "import json, yaml, jsonschema; \
  schema = yaml.safe_load(open('morphir-ir-v2.yaml')); \
  data = json.load(open('your-morphir-ir.json')); \
  jsonschema.validate(data, schema); \
  print('✓ Valid Morphir IR v2')"

References


Appendix: Complete Schema Definition

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
# JSON Schema for Morphir IR Format Version 2
# This schema defines the structure of a Morphir IR distribution in version 2 format.
# Format version 2 uses capitalized tag names (e.g., "Library", "Public", "Variable").

$schema: "http://json-schema.org/draft-07/schema#"
$id: "https://finos.github.io/morphir/schemas/morphir-ir-v2.yaml"
title: "Morphir IR Distribution (Version 2)"
description: |
  A Morphir IR distribution represents a complete, self-contained package of business logic
  with all its dependencies. It captures the semantics of functional programs in a
  language-independent, platform-agnostic format.
  
  This is format version 2, which differs from version 3 primarily in tag capitalization.

type: object
required:
  - formatVersion
  - distribution
properties:
  formatVersion:
    type: integer
    const: 2
    description: "The version of the IR format. Must be 2 for this schema."
  
  distribution:
    description: "The distribution data, currently only Library distributions are supported."
    type: array
    minItems: 4
    maxItems: 4
    items:
      - type: string
        const: "Library"
        description: "Distribution type (capitalized in v2)."
      - $ref: "#/definitions/PackageName"
      - $ref: "#/definitions/Dependencies"
      - $ref: "#/definitions/PackageDefinition"

definitions:
  # ===== Basic Building Blocks =====
  
  Name:
    type: array
    items:
      type: string
      pattern: "^[a-z][a-z0-9]*$"
    minItems: 1
    description: |
      A Name is a list of lowercase words that represents a human-readable identifier.
      Example: ["value", "in", "u", "s", "d"] can be rendered as valueInUSD or value_in_USD.
  
  Path:
    type: array
    items:
      $ref: "#/definitions/Name"
    minItems: 1
    description: |
      A Path is a list of Names representing a hierarchical location in the IR structure.
      Used for package names and module names.
  
  PackageName:
    $ref: "#/definitions/Path"
    description: "Globally unique identifier for a package."
  
  ModuleName:
    $ref: "#/definitions/Path"
    description: "Unique identifier for a module within a package."
  
  FQName:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - $ref: "#/definitions/PackageName"
      - $ref: "#/definitions/ModuleName"
      - $ref: "#/definitions/Name"
    description: |
      Fully-Qualified Name that provides a globally unique identifier for any type or value.
      Consists of [packagePath, modulePath, localName].
  
  # ===== Attributes =====
  
  Attributes:
    type: object
    description: |
      Attributes can be attached to various nodes in the IR for extensibility.
      When no additional information is needed, an empty object {} is used.
  
  # ===== Access Control =====
  
  AccessControlled:
    type: object
    required: ["access", "value"]
    properties:
      access:
        type: string
        enum: ["Public", "Private"]
        description: "Controls visibility of types and values (capitalized in v2)."
      value:
        description: "The value being access controlled."
    description: "Wrapper that manages visibility of types and values."
  
  # Note: Documented is not a separate schema definition because it's encoded conditionally.
  # When documentation exists, the JSON has both "doc" and "value" fields.
  # When documentation is absent, the JSON contains only the documented element directly (no wrapper).
  # This is handled inline in the definitions that use Documented.
  
  # ===== Distribution Structure =====
  
  Dependencies:
    type: array
    items:
      type: array
      minItems: 2
      maxItems: 2
      items:
        - $ref: "#/definitions/PackageName"
        - $ref: "#/definitions/PackageSpecification"
    description: "Dictionary of package dependencies, contains only type signatures."
  
  PackageDefinition:
    type: object
    required: ["modules"]
    properties:
      modules:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/ModuleName"
            - allOf:
                - $ref: "#/definitions/AccessControlled"
                - properties:
                    value:
                      $ref: "#/definitions/ModuleDefinition"
        description: "All modules in the package (public and private)."
    description: "Complete implementation of a package with all details."
  
  PackageSpecification:
    type: object
    required: ["modules"]
    properties:
      modules:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/ModuleName"
            - $ref: "#/definitions/ModuleSpecification"
        description: "Public modules only."
    description: "Public interface of a package, contains only type signatures."
  
  # ===== Module Structure =====
  
  ModuleDefinition:
    type: object
    required: ["types", "values"]
    properties:
      types:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - allOf:
                - $ref: "#/definitions/AccessControlled"
                - properties:
                    value:
                      # Documented wrapper: can have "doc" and "value", or just the type definition directly
                      oneOf:
                        - type: object
                          required: ["doc", "value"]
                          properties:
                            doc:
                              type: string
                            value:
                              $ref: "#/definitions/TypeDefinition"
                        - $ref: "#/definitions/TypeDefinition"
        description: "All type definitions (public and private)."
      values:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - allOf:
                - $ref: "#/definitions/AccessControlled"
                - properties:
                    value:
                      # Documented wrapper: can have "doc" and "value", or just the value definition directly
                      oneOf:
                        - type: object
                          required: ["doc", "value"]
                          properties:
                            doc:
                              type: string
                            value:
                              $ref: "#/definitions/ValueDefinition"
                        - $ref: "#/definitions/ValueDefinition"
        description: "All value definitions (public and private)."
      doc:
        type: string
        description: "Optional documentation for the module."
    description: "Complete implementation of a module."
  
  ModuleSpecification:
    type: object
    required: ["types", "values"]
    properties:
      types:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - oneOf:
                - type: object
                  required: ["doc", "value"]
                  properties:
                    doc:
                      type: string
                    value:
                      $ref: "#/definitions/TypeSpecification"
                - $ref: "#/definitions/TypeSpecification"
        description: "Public type specifications only."
      values:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - oneOf:
                - type: object
                  required: ["doc", "value"]
                  properties:
                    doc:
                      type: string
                    value:
                      $ref: "#/definitions/ValueSpecification"
                - $ref: "#/definitions/ValueSpecification"
        description: "Public value specifications only."
      doc:
        type: string
        description: "Optional documentation for the module."
    description: "Public interface of a module."
  
  # ===== Type System =====
  
  Type:
    description: |
      A Type is a recursive tree structure representing type expressions.
      Tags are capitalized in format version 2.
    oneOf:
      - $ref: "#/definitions/VariableType"
      - $ref: "#/definitions/ReferenceType"
      - $ref: "#/definitions/TupleType"
      - $ref: "#/definitions/RecordType"
      - $ref: "#/definitions/ExtensibleRecordType"
      - $ref: "#/definitions/FunctionType"
      - $ref: "#/definitions/UnitType"
  
  VariableType:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Variable"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
    description: "Represents a type variable (generic parameter)."
  
  ReferenceType:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "Reference"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
      - type: array
        items:
          $ref: "#/definitions/Type"
        description: "Type arguments for generic types."
    description: "Reference to another type or type alias."
  
  TupleType:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Tuple"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Type"
        description: "Element types in order."
    description: "A composition of multiple types in a fixed order (product type)."
  
  RecordType:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "Record"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Field"
        description: "List of field definitions."
    description: "A composition of named fields with their types."
  
  ExtensibleRecordType:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "ExtensibleRecord"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
      - type: array
        items:
          $ref: "#/definitions/Field"
        description: "Known fields."
    description: "A record type that can be extended with additional fields."
  
  FunctionType:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "Function"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Type"
      - $ref: "#/definitions/Type"
    description: |
      Represents a function type. Multi-argument functions are represented via currying.
      Items: [tag, attributes, argumentType, returnType]
  
  UnitType:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "Unit"
      - $ref: "#/definitions/Attributes"
    description: "The type with exactly one value (similar to void in some languages)."
  
  Field:
    type: object
    required: ["name", "tpe"]
    properties:
      name:
        $ref: "#/definitions/Name"
        description: "Field name."
      tpe:
        $ref: "#/definitions/Type"
        description: "Field type."
    description: "A field in a record type."
  
  # ===== Type Specifications =====
  
  TypeSpecification:
    description: "Defines the interface of a type without implementation details."
    oneOf:
      - $ref: "#/definitions/TypeAliasSpecification"
      - $ref: "#/definitions/OpaqueTypeSpecification"
      - $ref: "#/definitions/CustomTypeSpecification"
      - $ref: "#/definitions/DerivedTypeSpecification"
  
  TypeAliasSpecification:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "TypeAliasSpecification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - $ref: "#/definitions/Type"
    description: "An alias for another type."
  
  OpaqueTypeSpecification:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "OpaqueTypeSpecification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
    description: |
      A type with unknown structure. The implementation is hidden from consumers.
  
  CustomTypeSpecification:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "CustomTypeSpecification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - $ref: "#/definitions/Constructors"
    description: "A tagged union type (sum type)."
  
  DerivedTypeSpecification:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "DerivedTypeSpecification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - type: object
        required: ["baseType", "fromBaseType", "toBaseType"]
        properties:
          baseType:
            $ref: "#/definitions/Type"
            description: "The type used for serialization."
          fromBaseType:
            $ref: "#/definitions/FQName"
            description: "Function to convert from base type."
          toBaseType:
            $ref: "#/definitions/FQName"
            description: "Function to convert to base type."
        description: "Details for derived type."
    description: |
      A type with platform-specific representation but known serialization.
  
  # ===== Type Definitions =====
  
  TypeDefinition:
    description: "Provides the complete implementation of a type."
    oneOf:
      - $ref: "#/definitions/TypeAliasDefinition"
      - $ref: "#/definitions/CustomTypeDefinition"
  
  TypeAliasDefinition:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "TypeAliasDefinition"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - $ref: "#/definitions/Type"
    description: "Complete definition of a type alias."
  
  CustomTypeDefinition:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "CustomTypeDefinition"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - allOf:
          - $ref: "#/definitions/AccessControlled"
          - properties:
              value:
                $ref: "#/definitions/Constructors"
    description: |
      Complete definition of a custom type. If constructors are Private, 
      the specification becomes OpaqueTypeSpecification.
  
  Constructors:
    type: array
    items:
      type: array
      minItems: 2
      maxItems: 2
      items:
        - $ref: "#/definitions/Name"
        - type: array
          items:
            type: array
            minItems: 2
            maxItems: 2
            items:
              - $ref: "#/definitions/Name"
              - $ref: "#/definitions/Type"
          description: "Constructor arguments as (name, type) pairs."
    description: "Dictionary of constructor names to their typed arguments."
  
  # ===== Value System =====
  # Value expressions use lowercase tags in v2 (e.g., "apply", "lambda")
  
  Value:
    description: |
      A Value is a recursive tree structure representing computations.
      All data and logic in Morphir are represented as value expressions.
      Note: Value tags are lowercase in format version 2.
    oneOf:
      - $ref: "#/definitions/LiteralValue"
      - $ref: "#/definitions/ConstructorValue"
      - $ref: "#/definitions/TupleValue"
      - $ref: "#/definitions/ListValue"
      - $ref: "#/definitions/RecordValue"
      - $ref: "#/definitions/VariableValue"
      - $ref: "#/definitions/ReferenceValue"
      - $ref: "#/definitions/FieldValue"
      - $ref: "#/definitions/FieldFunctionValue"
      - $ref: "#/definitions/ApplyValue"
      - $ref: "#/definitions/LambdaValue"
      - $ref: "#/definitions/LetDefinitionValue"
      - $ref: "#/definitions/LetRecursionValue"
      - $ref: "#/definitions/DestructureValue"
      - $ref: "#/definitions/IfThenElseValue"
      - $ref: "#/definitions/PatternMatchValue"
      - $ref: "#/definitions/UpdateRecordValue"
      - $ref: "#/definitions/UnitValue"
  
  LiteralValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "literal"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Literal"
    description: "A literal constant value."
  
  ConstructorValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "constructor"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
    description: "Reference to a custom type constructor."
  
  TupleValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "tuple"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Value"
        description: "Element values in order."
    description: "A tuple value with multiple elements."
  
  ListValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "list"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Value"
        description: "List elements."
    description: "A list of values."
  
  RecordValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "record"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Value"
        description: "Dictionary mapping field names to values."
    description: "A record value with named fields."
  
  VariableValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "variable"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
    description: "Reference to a variable in scope."
  
  ReferenceValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "reference"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
    description: "Reference to a defined value (function or constant)."
  
  FieldValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "field"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Name"
    description: "Field access on a record. Items: [tag, attributes, recordExpr, fieldName]"
  
  FieldFunctionValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "field_function"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
    description: "A function that extracts a field (e.g., .firstName)."
  
  ApplyValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "apply"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
    description: |
      Function application. Items: [tag, attributes, function, argument].
      Multi-argument calls are represented via currying (nested Apply nodes).
  
  LambdaValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "lambda"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Value"
    description: |
      Anonymous function (lambda abstraction).
      Items: [tag, attributes, argumentPattern, body]
  
  LetDefinitionValue:
    type: array
    minItems: 5
    maxItems: 5
    items:
      - const: "let_definition"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
      - $ref: "#/definitions/ValueDefinition"
      - $ref: "#/definitions/Value"
    description: |
      A let binding introducing a single value.
      Items: [tag, attributes, bindingName, definition, inExpr]
  
  LetRecursionValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "let_recursion"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/ValueDefinition"
        description: "Multiple bindings that can reference each other."
      - $ref: "#/definitions/Value"
    description: "Mutually recursive let bindings."
  
  DestructureValue:
    type: array
    minItems: 5
    maxItems: 5
    items:
      - const: "destructure"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
    description: |
      Pattern-based destructuring.
      Items: [tag, attributes, pattern, valueToDestructure, inExpr]
  
  IfThenElseValue:
    type: array
    minItems: 5
    maxItems: 5
    items:
      - const: "if_then_else"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
    description: |
      Conditional expression.
      Items: [tag, attributes, condition, thenBranch, elseBranch]
  
  PatternMatchValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "pattern_match"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Pattern"
            - $ref: "#/definitions/Value"
        description: "List of pattern-branch pairs."
    description: "Pattern matching with multiple cases."
  
  UpdateRecordValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "update_record"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Value"
        description: "Fields to update with new values."
    description: |
      Record update expression (immutable copy-on-update).
      Items: [tag, attributes, recordToUpdate, fieldsToUpdate]
  
  UnitValue:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "unit"
      - $ref: "#/definitions/Attributes"
    description: "The unit value (the single value of the Unit type)."
  
  # ===== Literals =====
  
  Literal:
    description: "Represents literal constant values."
    oneOf:
      - $ref: "#/definitions/BoolLiteral"
      - $ref: "#/definitions/CharLiteral"
      - $ref: "#/definitions/StringLiteral"
      - $ref: "#/definitions/WholeNumberLiteral"
      - $ref: "#/definitions/FloatLiteral"
      - $ref: "#/definitions/DecimalLiteral"
  
  BoolLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "bool_literal"
      - type: boolean
    description: "Boolean literal (true or false)."
  
  CharLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "char_literal"
      - type: string
        minLength: 1
        maxLength: 1
    description: "Single character literal."
  
  StringLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "string_literal"
      - type: string
    description: "Text string literal."
  
  WholeNumberLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "whole_number_literal"
      - type: integer
    description: "Integer literal."
  
  FloatLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "float_literal"
      - type: number
    description: "Floating-point number literal."
  
  DecimalLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "decimal_literal"
      - type: string
        pattern: "^-?[0-9]+(\\.[0-9]+)?$"
    description: "Arbitrary-precision decimal literal (stored as string)."
  
  # ===== Patterns =====
  
  Pattern:
    description: |
      Patterns are used for destructuring and filtering values.
      They appear in lambda, let destructure, and pattern match expressions.
      Pattern tags are lowercase with underscores in format version 2.
    oneOf:
      - $ref: "#/definitions/WildcardPattern"
      - $ref: "#/definitions/AsPattern"
      - $ref: "#/definitions/TuplePattern"
      - $ref: "#/definitions/ConstructorPattern"
      - $ref: "#/definitions/EmptyListPattern"
      - $ref: "#/definitions/HeadTailPattern"
      - $ref: "#/definitions/LiteralPattern"
      - $ref: "#/definitions/UnitPattern"
  
  WildcardPattern:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "wildcard_pattern"
      - $ref: "#/definitions/Attributes"
    description: "Matches any value without binding (the _ pattern)."
  
  AsPattern:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "as_pattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Name"
    description: |
      Binds a name to a value matched by a nested pattern.
      Simple variable binding is AsPattern with WildcardPattern nested.
      Items: [tag, attributes, nestedPattern, variableName]
  
  TuplePattern:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "tuple_pattern"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Pattern"
        description: "Patterns for each tuple element."
    description: "Matches a tuple by matching each element."
  
  ConstructorPattern:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "constructor_pattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
      - type: array
        items:
          $ref: "#/definitions/Pattern"
        description: "Patterns for constructor arguments."
    description: "Matches a specific type constructor and its arguments."
  
  EmptyListPattern:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "empty_list_pattern"
      - $ref: "#/definitions/Attributes"
    description: "Matches an empty list (the [] pattern)."
  
  HeadTailPattern:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "head_tail_pattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Pattern"
    description: |
      Matches a non-empty list by head and tail (the x :: xs pattern).
      Items: [tag, attributes, headPattern, tailPattern]
  
  LiteralPattern:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "literal_pattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Literal"
    description: "Matches an exact literal value."
  
  UnitPattern:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "unit_pattern"
      - $ref: "#/definitions/Attributes"
    description: "Matches the unit value."
  
  # ===== Value Specifications and Definitions =====
  
  ValueSpecification:
    type: object
    required: ["inputs", "output"]
    properties:
      inputs:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Type"
        description: "Function parameters as (name, type) pairs."
      output:
        $ref: "#/definitions/Type"
        description: "The return type."
    description: |
      The type signature of a value or function.
      Contains only type information, no implementation.
  
  ValueDefinition:
    type: object
    required: ["inputTypes", "outputType", "body"]
    properties:
      inputTypes:
        type: array
        items:
          type: array
          minItems: 3
          maxItems: 3
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Attributes"
            - $ref: "#/definitions/Type"
        description: "Function parameters as (name, attributes, type) tuples."
      outputType:
        $ref: "#/definitions/Type"
        description: "The return type."
      body:
        $ref: "#/definitions/Value"
        description: "The value expression implementing the logic."
    description: |
      The complete implementation of a value or function.
      Contains both type information and implementation.

4.2.3 - Schema Version 1

Morphir IR JSON Schema for format version 1

Morphir IR Schema - Version 1

Format version 1 is the original Morphir IR format. It uses lowercase tag names throughout and has a different module structure compared to later versions.

Overview

Version 1 of the Morphir IR format uses lowercase tags for all constructs. This includes distribution types, access control levels, type tags, value expression tags, pattern tags, and literal tags.

Key Characteristics

Tag Capitalization

All tags in version 1 are lowercase:

  • Distribution: "library" (not "Library")
  • Access Control: "public" and "private" (not "Public" and "Private")
  • Type Tags: "variable", "reference", "tuple", "record", etc.
  • Value Tags: "apply", "lambda", "let_definition", etc.
  • Pattern Tags: "as_pattern", "wildcard_pattern", "constructor_pattern", etc.
  • Literal Tags: "bool_literal", "string_literal", "whole_number_literal", etc.

Module Structure

In version 1, modules are represented as objects with name and def fields:

ModuleEntry:
  type: object
  required: ["name", "def"]
  properties:
    name:
      $ref: "#/definitions/ModuleName"
    def:
      type: array
      minItems: 2
      maxItems: 2
      items:
        - $ref: "#/definitions/AccessLevel"
        - $ref: "#/definitions/ModuleDefinition"

This differs from version 2+, where modules are represented as arrays: [modulePath, accessControlled].

Core Concepts

Naming System

The Morphir IR uses a sophisticated naming system independent of any specific naming convention.

Name

A Name represents a human-readable identifier made up of one or more words.

  • Structure: Array of lowercase word strings
  • Purpose: Atomic unit for all identifiers
  • Example: ["value", "in", "u", "s", "d"] renders as valueInUSD or value_in_USD
Name:
  type: array
  items:
    type: string
    pattern: "^[a-z][a-z0-9]*$"
  minItems: 1
  description: |
    A Name is a list of lowercase words that represents a human-readable identifier.
    Example: ["value", "in", "u", "s", "d"] can be rendered as valueInUSD or value_in_USD.

Path

A Path represents a hierarchical location in the IR structure.

  • Structure: List of Names
  • Purpose: Identifies packages and modules
  • Example: [["morphir"], ["s", "d", "k"], ["string"]] for the String module
Path:
  type: array
  items:
    $ref: "#/definitions/Name"
  minItems: 1
  description: |
    A Path is a list of Names representing a hierarchical location in the IR structure.

Fully-Qualified Name (FQName)

Provides globally unique identifiers for types and values.

  • Structure: [packagePath, modulePath, localName]
  • Purpose: Unambiguous references across package boundaries
FQName:
  type: array
  minItems: 3
  maxItems: 3
  items:
    - $ref: "#/definitions/PackageName"
    - $ref: "#/definitions/ModuleName"
    - $ref: "#/definitions/Name"

Access Control

Access Levels

Manages visibility of types and values.

  • Levels: "public" (visible externally) or "private" (package-only)
  • Purpose: Controls API exposure
  • Version 1 note: Lowercase access levels ("public", "private")
AccessLevel:
  type: string
  enum: ["public", "private"]

Distribution and Package Structure

Distribution

A Distribution represents a complete, self-contained package with all dependencies.

  • Current type: library (only supported distribution type)
  • Structure: ["library", packageName, dependencies, packageDefinition]
  • Purpose: Output of compilation process, ready for execution or transformation
  • Version 1 note: Uses lowercase "library" tag
distribution:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - type: string
      const: "library"
    - $ref: "#/definitions/PackageName"
    - $ref: "#/definitions/Dependencies"
    - $ref: "#/definitions/PackageDefinition"

Package Definition

Complete implementation of a package with all details.

  • Contains: All modules (public and private)
  • Includes: Type signatures and implementations
  • Purpose: Full package representation for processing
  • Version 1 note: Modules stored as objects with {"name": ..., "def": [accessLevel, moduleDefinition]}

Package Specification

Public interface of a package.

  • Contains: Only publicly exposed modules
  • Includes: Only type signatures, no implementations
  • Purpose: Dependency interface

Module Structure Details

Module Entry (Version 1 specific)

In version 1, modules use an object structure with explicit name and def fields:

ModuleEntry:
  type: object
  required: ["name", "def"]
  properties:
    name:
      $ref: "#/definitions/ModuleName"
    def:
      type: array
      minItems: 2
      maxItems: 2
      items:
        - $ref: "#/definitions/AccessLevel"
        - $ref: "#/definitions/ModuleDefinition"

Module Definition

Complete implementation of a module.

  • Contains: All types and values (public and private) with implementations
  • Structure: Dictionary of type names to type definitions, and value names to value definitions
  • Purpose: Complete module implementation

Module Specification

Public interface of a module.

  • Contains: Only publicly exposed types and values
  • Includes: Type signatures only, no implementations
  • Purpose: Module’s public API

Type System

The type system is based on functional programming principles, supporting:

Version 1 note: All type tags are lowercase in version 1.

Type Expressions

variable

Represents a type variable (generic parameter).

  • Structure: ["variable", attributes, name]
  • Example: The a in List a
  • Purpose: Enables polymorphic types
VariableType:
  type: array
  minItems: 3
  maxItems: 3
  items:
    - const: "variable"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/Name"

reference

Reference to another type or type alias.

  • Structure: ["reference", attributes, fqName, typeArgs]
  • Examples: String, List Int, Maybe a
  • Purpose: References built-in types, custom types, or type aliases
ReferenceType:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - const: "reference"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/FQName"
    - type: array
      items:
        $ref: "#/definitions/Type"

tuple

Composition of multiple types in fixed order.

  • Structure: ["tuple", attributes, elementTypes]
  • Example: (Int, String, Bool)
  • Purpose: Product types with positional access

record

Composition of named fields with types.

  • Structure: ["record", attributes, fields]
  • Example: {firstName: String, age: Int}
  • Purpose: Product types with named field access
  • Note: All fields are required

function

Function type representation.

  • Structure: ["function", attributes, argType, returnType]
  • Example: Int -> String
  • Purpose: Represents function and lambda types
  • Note: Multi-argument functions use currying (nested function types)

Type Specifications

type_alias_specification

An alias for another type.

  • Structure: ["type_alias_specification", typeParams, aliasedType]
  • Example: type alias UserId = String
  • Purpose: Meaningful name for type expression

custom_type_specification

Tagged union type (sum type).

  • Structure: ["custom_type_specification", typeParams, constructors]
  • Example: type Result e a = Ok a | Err e
  • Purpose: Choice between multiple alternatives

opaque_type_specification

Type with unknown structure.

  • Structure: ["opaque_type_specification", typeParams]
  • Characteristics: Structure hidden, no automatic serialization
  • Purpose: Encapsulates implementation details

Value System

All data and logic in Morphir are represented as value expressions.

Version 1 note: All value tags are lowercase in version 1.

Value Expressions

literal

Literal constant value.

  • Structure: ["literal", attributes, literal]
  • Types: bool_literal, char_literal, string_literal, whole_number_literal, float_literal, decimal_literal
  • Purpose: Represents constant data

variable

Reference to a variable in scope.

  • Structure: ["variable", attributes, name]
  • Example: References to function parameters or let-bound variables
  • Purpose: Accesses values bound in current scope

reference

Reference to a defined value (function or constant).

  • Structure: ["reference", attributes, fqName]
  • Example: Morphir.SDK.List.map, Basics.add
  • Purpose: Invokes or references defined functions

apply

Function application.

  • Structure: ["apply", attributes, function, argument]
  • Example: add 1 2 (nested apply nodes for currying)
  • Purpose: Invokes functions with arguments
ApplyValue:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - const: "apply"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/Value"
    - $ref: "#/definitions/Value"

lambda

Anonymous function.

  • Structure: ["lambda", attributes, argumentPattern, body]
  • Example: \x -> x + 1
  • Purpose: Creates inline functions
LambdaValue:
  type: array
  minItems: 4
  maxItems: 4
  items:
    - const: "lambda"
    - $ref: "#/definitions/Attributes"
    - $ref: "#/definitions/Pattern"
    - $ref: "#/definitions/Value"

let_definition

Let binding introducing a single value.

  • Structure: ["let_definition", attributes, bindingName, definition, inExpr]
  • Example: let x = 5 in x + x
  • Purpose: Introduces local bindings

if_then_else

Conditional expression.

  • Structure: ["if_then_else", attributes, condition, thenBranch, elseBranch]
  • Example: if x > 0 then "positive" else "non-positive"
  • Purpose: Conditional logic

pattern_match

Pattern matching with multiple cases.

  • Structure: ["pattern_match", attributes, valueToMatch, cases]
  • Example: case maybeValue of Just x -> x; Nothing -> 0
  • Purpose: Conditional logic based on structure

Patterns

Used for destructuring and filtering values.

Version 1 note: All pattern tags are lowercase in version 1.

wildcard_pattern

Matches any value without binding.

  • Structure: ["wildcard_pattern", attributes]
  • Syntax: _
  • Purpose: Ignores a value

as_pattern

Binds a name to a matched value.

  • Structure: ["as_pattern", attributes, nestedPattern, variableName]
  • Special case: Simple variable binding uses as_pattern with wildcard_pattern
  • Purpose: Captures matched values

constructor_pattern

Matches specific type constructor and arguments.

  • Structure: ["constructor_pattern", attributes, fqName, argPatterns]
  • Example: Just x matches Just with pattern x
  • Purpose: Destructures and filters tagged unions

Literals

Version 1 note: All literal tags are lowercase in version 1.

bool_literal

Boolean literal.

  • Structure: ["bool_literal", boolean]
  • Values: true or false
BoolLiteral:
  type: array
  minItems: 2
  maxItems: 2
  items:
    - const: "bool_literal"
    - type: boolean

string_literal

Text string literal.

  • Structure: ["string_literal", string]
  • Example: "hello"
StringLiteral:
  type: array
  minItems: 2
  maxItems: 2
  items:
    - const: "string_literal"
    - type: string

whole_number_literal

Integer literal.

  • Structure: ["whole_number_literal", integer]
  • Example: 42, -17

Migration from Version 1

When migrating from version 1 to version 2 or 3:

  1. Capitalize distribution tag: "library" → "Library"
  2. Capitalize access control: "public" → "Public", "private" → "Private"
  3. Update module structure: Convert {"name": ..., "def": ...} to [modulePath, accessControlled]
  4. Capitalize type tags: "variable" → "Variable", etc.
  5. For version 3: Also capitalize value and pattern tags

Full Schema

For the complete schema definition, see the full schema page.

References

4.2.3.1 - Full Schema

Complete Morphir IR JSON Schema for format version 1

Morphir IR Schema Version 1 - Complete Schema

This page contains the complete JSON schema definition for Morphir IR format version 1.

Download

You can download the schema file directly: morphir-ir-v1.yaml

Usage

This schema can be used to validate Morphir IR JSON files in format version 1:

# Using Python jsonschema (recommended for YAML schemas)
pip install jsonschema pyyaml
python -c "import json, yaml, jsonschema; \
  schema = yaml.safe_load(open('morphir-ir-v1.yaml')); \
  data = json.load(open('your-morphir-ir.json')); \
  jsonschema.validate(data, schema); \
  print('✓ Valid Morphir IR v1')"

References


Appendix: Complete Schema Definition

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
# JSON Schema for Morphir IR Format Version 1
# This schema defines the structure of a Morphir IR distribution in version 1 format.
# Format version 1 uses lowercase tag names and different structure for modules.

$schema: "http://json-schema.org/draft-07/schema#"
$id: "https://finos.github.io/morphir/schemas/morphir-ir-v1.yaml"
title: "Morphir IR Distribution (Version 1)"
description: |
  A Morphir IR distribution represents a complete, self-contained package of business logic
  with all its dependencies. It captures the semantics of functional programs in a
  language-independent, platform-agnostic format.
  
  This is format version 1, which uses lowercase tags and a different module structure.

type: object
required:
  - formatVersion
  - distribution
properties:
  formatVersion:
    type: integer
    const: 1
    description: "The version of the IR format. Must be 1 for this schema."
  
  distribution:
    description: "The distribution data, currently only Library distributions are supported."
    type: array
    minItems: 4
    maxItems: 4
    items:
      - type: string
        const: "library"
        description: "Distribution type (lowercase in v1)."
      - $ref: "#/definitions/PackageName"
      - $ref: "#/definitions/Dependencies"
      - $ref: "#/definitions/PackageDefinition"

definitions:
  # ===== Basic Building Blocks =====
  
  Name:
    type: array
    items:
      type: string
      pattern: "^[a-z][a-z0-9]*$"
    minItems: 1
    description: |
      A Name is a list of lowercase words that represents a human-readable identifier.
      Example: ["value", "in", "u", "s", "d"] can be rendered as valueInUSD or value_in_USD.
  
  Path:
    type: array
    items:
      $ref: "#/definitions/Name"
    minItems: 1
    description: |
      A Path is a list of Names representing a hierarchical location in the IR structure.
      Used for package names and module names.
  
  PackageName:
    $ref: "#/definitions/Path"
    description: "Globally unique identifier for a package."
  
  ModuleName:
    $ref: "#/definitions/Path"
    description: "Unique identifier for a module within a package."
  
  FQName:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - $ref: "#/definitions/PackageName"
      - $ref: "#/definitions/ModuleName"
      - $ref: "#/definitions/Name"
    description: |
      Fully-Qualified Name that provides a globally unique identifier for any type or value.
      Consists of [packagePath, modulePath, localName].
  
  # ===== Attributes =====
  
  Attributes:
    type: object
    description: |
      Attributes can be attached to various nodes in the IR for extensibility.
      When no additional information is needed, an empty object {} is used.
  
  # ===== Access Control (V1 format) =====
  
  AccessLevel:
    type: string
    enum: ["public", "private"]
    description: "Controls visibility of types and values (lowercase in v1)."
  
  # Note: Documented is not a separate schema definition because it's encoded conditionally.
  # When documentation exists, the JSON has both "doc" and "value" fields.
  # When documentation is absent, the JSON contains only the documented element directly (no wrapper).
  # This is handled inline in the definitions that use Documented.
  
  # ===== Distribution Structure =====
  
  Dependencies:
    type: array
    items:
      type: array
      minItems: 2
      maxItems: 2
      items:
        - $ref: "#/definitions/PackageName"
        - $ref: "#/definitions/PackageSpecification"
    description: "Dictionary of package dependencies, contains only type signatures."
  
  PackageDefinition:
    type: object
    required: ["modules"]
    properties:
      modules:
        type: array
        items:
          $ref: "#/definitions/ModuleEntry"
        description: "All modules in the package (public and private)."
    description: "Complete implementation of a package with all details."
  
  ModuleEntry:
    type: object
    required: ["name", "def"]
    properties:
      name:
        $ref: "#/definitions/ModuleName"
        description: "The module name/path."
      def:
        type: array
        minItems: 2
        maxItems: 2
        items:
          - $ref: "#/definitions/AccessLevel"
          - $ref: "#/definitions/ModuleDefinition"
        description: "Access-controlled module definition [accessLevel, definition]."
    description: "Module entry with name and access-controlled definition (v1 format)."
  
  PackageSpecification:
    type: object
    required: ["modules"]
    properties:
      modules:
        type: array
        items:
          type: object
          required: ["name", "spec"]
          properties:
            name:
              $ref: "#/definitions/ModuleName"
              description: "The module name/path."
            spec:
              $ref: "#/definitions/ModuleSpecification"
              description: "The module specification."
        description: "Public modules only."
    description: "Public interface of a package, contains only type signatures."
  
  # ===== Module Structure =====
  
  ModuleDefinition:
    type: object
    required: ["types", "values"]
    properties:
      types:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - type: array
              minItems: 2
              maxItems: 2
              items:
                - $ref: "#/definitions/AccessLevel"
                - oneOf:
                    - type: object
                      required: ["doc", "value"]
                      properties:
                        doc:
                          type: string
                        value:
                          $ref: "#/definitions/TypeDefinition"
                    - $ref: "#/definitions/TypeDefinition"
        description: "All type definitions (public and private)."
      values:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - type: array
              minItems: 2
              maxItems: 2
              items:
                - $ref: "#/definitions/AccessLevel"
                - oneOf:
                    - type: object
                      required: ["doc", "value"]
                      properties:
                        doc:
                          type: string
                        value:
                          $ref: "#/definitions/ValueDefinition"
                    - $ref: "#/definitions/ValueDefinition"
        description: "All value definitions (public and private)."
      doc:
        type: string
        description: "Optional documentation for the module."
    description: "Complete implementation of a module."
  
  ModuleSpecification:
    type: object
    required: ["types", "values"]
    properties:
      types:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - oneOf:
                - type: object
                  required: ["doc", "value"]
                  properties:
                    doc:
                      type: string
                    value:
                      $ref: "#/definitions/TypeSpecification"
                - $ref: "#/definitions/TypeSpecification"
        description: "Public type specifications only."
      values:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - oneOf:
                - type: object
                  required: ["doc", "value"]
                  properties:
                    doc:
                      type: string
                    value:
                      $ref: "#/definitions/ValueSpecification"
                - $ref: "#/definitions/ValueSpecification"
        description: "Public value specifications only."
      doc:
        type: string
        description: "Optional documentation for the module."
    description: "Public interface of a module."
  
  # ===== Type System =====
  # All type tags are lowercase in v1
  
  Type:
    description: |
      A Type is a recursive tree structure representing type expressions.
      Tags are lowercase in format version 1.
    oneOf:
      - $ref: "#/definitions/VariableType"
      - $ref: "#/definitions/ReferenceType"
      - $ref: "#/definitions/TupleType"
      - $ref: "#/definitions/RecordType"
      - $ref: "#/definitions/ExtensibleRecordType"
      - $ref: "#/definitions/FunctionType"
      - $ref: "#/definitions/UnitType"
  
  VariableType:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "variable"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
    description: "Represents a type variable (generic parameter)."
  
  ReferenceType:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "reference"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
      - type: array
        items:
          $ref: "#/definitions/Type"
        description: "Type arguments for generic types."
    description: "Reference to another type or type alias."
  
  TupleType:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "tuple"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Type"
        description: "Element types in order."
    description: "A composition of multiple types in a fixed order (product type)."
  
  RecordType:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "record"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Field"
        description: "List of field definitions."
    description: "A composition of named fields with their types."
  
  ExtensibleRecordType:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "extensible_record"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
      - type: array
        items:
          $ref: "#/definitions/Field"
        description: "Known fields."
    description: "A record type that can be extended with additional fields."
  
  FunctionType:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "function"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Type"
      - $ref: "#/definitions/Type"
    description: |
      Represents a function type. Multi-argument functions are represented via currying.
      Items: [tag, attributes, argumentType, returnType]
  
  UnitType:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "unit"
      - $ref: "#/definitions/Attributes"
    description: "The type with exactly one value (similar to void in some languages)."
  
  Field:
    type: object
    required: ["name", "tpe"]
    properties:
      name:
        $ref: "#/definitions/Name"
        description: "Field name."
      tpe:
        $ref: "#/definitions/Type"
        description: "Field type."
    description: "A field in a record type."
  
  # ===== Type Specifications =====
  # All type specification tags are lowercase with underscores in v1
  
  TypeSpecification:
    description: "Defines the interface of a type without implementation details."
    oneOf:
      - $ref: "#/definitions/TypeAliasSpecification"
      - $ref: "#/definitions/OpaqueTypeSpecification"
      - $ref: "#/definitions/CustomTypeSpecification"
      - $ref: "#/definitions/DerivedTypeSpecification"
  
  TypeAliasSpecification:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "type_alias_specification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - $ref: "#/definitions/Type"
    description: "An alias for another type."
  
  OpaqueTypeSpecification:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "opaque_type_specification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
    description: |
      A type with unknown structure. The implementation is hidden from consumers.
  
  CustomTypeSpecification:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "custom_type_specification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - $ref: "#/definitions/Constructors"
    description: "A tagged union type (sum type)."
  
  DerivedTypeSpecification:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "derived_type_specification"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - type: object
        required: ["baseType", "fromBaseType", "toBaseType"]
        properties:
          baseType:
            $ref: "#/definitions/Type"
            description: "The type used for serialization."
          fromBaseType:
            $ref: "#/definitions/FQName"
            description: "Function to convert from base type."
          toBaseType:
            $ref: "#/definitions/FQName"
            description: "Function to convert to base type."
        description: "Details for derived type."
    description: |
      A type with platform-specific representation but known serialization.
  
  # ===== Type Definitions =====
  # All type definition tags are lowercase with underscores in v1
  
  TypeDefinition:
    description: "Provides the complete implementation of a type."
    oneOf:
      - $ref: "#/definitions/TypeAliasDefinition"
      - $ref: "#/definitions/CustomTypeDefinition"
  
  TypeAliasDefinition:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "type_alias_definition"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - $ref: "#/definitions/Type"
    description: "Complete definition of a type alias."
  
  CustomTypeDefinition:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "custom_type_definition"
      - type: array
        items:
          $ref: "#/definitions/Name"
        description: "Type parameters."
      - type: array
        minItems: 2
        maxItems: 2
        items:
          - $ref: "#/definitions/AccessLevel"
          - $ref: "#/definitions/Constructors"
    description: |
      Complete definition of a custom type. If constructors are private, 
      the specification becomes opaque_type_specification.
  
  Constructors:
    type: array
    items:
      type: array
      minItems: 2
      maxItems: 2
      items:
        - $ref: "#/definitions/Name"
        - type: array
          items:
            type: array
            minItems: 2
            maxItems: 2
            items:
              - $ref: "#/definitions/Name"
              - $ref: "#/definitions/Type"
          description: "Constructor arguments as (name, type) pairs."
    description: "Dictionary of constructor names to their typed arguments."
  
  # ===== Value System =====
  # Value expressions use lowercase tags with underscores in v1
  
  Value:
    description: |
      A Value is a recursive tree structure representing computations.
      All data and logic in Morphir are represented as value expressions.
      Note: Value tags are lowercase with underscores in format version 1.
    oneOf:
      - $ref: "#/definitions/LiteralValue"
      - $ref: "#/definitions/ConstructorValue"
      - $ref: "#/definitions/TupleValue"
      - $ref: "#/definitions/ListValue"
      - $ref: "#/definitions/RecordValue"
      - $ref: "#/definitions/VariableValue"
      - $ref: "#/definitions/ReferenceValue"
      - $ref: "#/definitions/FieldValue"
      - $ref: "#/definitions/FieldFunctionValue"
      - $ref: "#/definitions/ApplyValue"
      - $ref: "#/definitions/LambdaValue"
      - $ref: "#/definitions/LetDefinitionValue"
      - $ref: "#/definitions/LetRecursionValue"
      - $ref: "#/definitions/DestructureValue"
      - $ref: "#/definitions/IfThenElseValue"
      - $ref: "#/definitions/PatternMatchValue"
      - $ref: "#/definitions/UpdateRecordValue"
      - $ref: "#/definitions/UnitValue"
  
  LiteralValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "literal"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Literal"
    description: "A literal constant value."
  
  ConstructorValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "constructor"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
    description: "Reference to a custom type constructor."
  
  TupleValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "tuple"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Value"
        description: "Element values in order."
    description: "A tuple value with multiple elements."
  
  ListValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "list"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Value"
        description: "List elements."
    description: "A list of values."
  
  RecordValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "record"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Value"
        description: "Dictionary mapping field names to values."
    description: "A record value with named fields."
  
  VariableValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "variable"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
    description: "Reference to a variable in scope."
  
  ReferenceValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "reference"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
    description: "Reference to a defined value (function or constant)."
  
  FieldValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "field"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Name"
    description: "Field access on a record. Items: [tag, attributes, recordExpr, fieldName]"
  
  FieldFunctionValue:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "field_function"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
    description: "A function that extracts a field (e.g., .firstName)."
  
  ApplyValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "apply"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
    description: |
      Function application. Items: [tag, attributes, function, argument].
      Multi-argument calls are represented via currying (nested Apply nodes).
  
  LambdaValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "lambda"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Value"
    description: |
      Anonymous function (lambda abstraction).
      Items: [tag, attributes, argumentPattern, body]
  
  LetDefinitionValue:
    type: array
    minItems: 5
    maxItems: 5
    items:
      - const: "let_definition"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Name"
      - $ref: "#/definitions/ValueDefinition"
      - $ref: "#/definitions/Value"
    description: |
      A let binding introducing a single value.
      Items: [tag, attributes, bindingName, definition, inExpr]
  
  LetRecursionValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "let_recursion"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/ValueDefinition"
        description: "Multiple bindings that can reference each other."
      - $ref: "#/definitions/Value"
    description: "Mutually recursive let bindings."
  
  DestructureValue:
    type: array
    minItems: 5
    maxItems: 5
    items:
      - const: "destructure"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
    description: |
      Pattern-based destructuring.
      Items: [tag, attributes, pattern, valueToDestructure, inExpr]
  
  IfThenElseValue:
    type: array
    minItems: 5
    maxItems: 5
    items:
      - const: "if_then_else"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
      - $ref: "#/definitions/Value"
    description: |
      Conditional expression.
      Items: [tag, attributes, condition, thenBranch, elseBranch]
  
  PatternMatchValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "pattern_match"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Pattern"
            - $ref: "#/definitions/Value"
        description: "List of pattern-branch pairs."
    description: "Pattern matching with multiple cases."
  
  UpdateRecordValue:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "update_record"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Value"
      - type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Value"
        description: "Fields to update with new values."
    description: |
      Record update expression (immutable copy-on-update).
      Items: [tag, attributes, recordToUpdate, fieldsToUpdate]
  
  UnitValue:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "unit"
      - $ref: "#/definitions/Attributes"
    description: "The unit value (the single value of the Unit type)."
  
  # ===== Literals =====
  # All literal tags are lowercase with underscores in v1
  
  Literal:
    description: "Represents literal constant values."
    oneOf:
      - $ref: "#/definitions/BoolLiteral"
      - $ref: "#/definitions/CharLiteral"
      - $ref: "#/definitions/StringLiteral"
      - $ref: "#/definitions/WholeNumberLiteral"
      - $ref: "#/definitions/FloatLiteral"
      - $ref: "#/definitions/DecimalLiteral"
  
  BoolLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "bool_literal"
      - type: boolean
    description: "Boolean literal (true or false)."
  
  CharLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "char_literal"
      - type: string
        minLength: 1
        maxLength: 1
    description: "Single character literal."
  
  StringLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "string_literal"
      - type: string
    description: "Text string literal."
  
  WholeNumberLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "whole_number_literal"
      - type: integer
    description: "Integer literal."
  
  FloatLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "float_literal"
      - type: number
    description: "Floating-point number literal."
  
  DecimalLiteral:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "decimal_literal"
      - type: string
        pattern: "^-?[0-9]+(\\.[0-9]+)?$"
    description: "Arbitrary-precision decimal literal (stored as string)."
  
  # ===== Patterns =====
  # All pattern tags are lowercase with underscores in v1
  
  Pattern:
    description: |
      Patterns are used for destructuring and filtering values.
      They appear in lambda, let destructure, and pattern match expressions.
      Pattern tags are lowercase with underscores in format version 1.
    oneOf:
      - $ref: "#/definitions/WildcardPattern"
      - $ref: "#/definitions/AsPattern"
      - $ref: "#/definitions/TuplePattern"
      - $ref: "#/definitions/ConstructorPattern"
      - $ref: "#/definitions/EmptyListPattern"
      - $ref: "#/definitions/HeadTailPattern"
      - $ref: "#/definitions/LiteralPattern"
      - $ref: "#/definitions/UnitPattern"
  
  WildcardPattern:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "wildcard_pattern"
      - $ref: "#/definitions/Attributes"
    description: "Matches any value without binding (the _ pattern)."
  
  AsPattern:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "as_pattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Name"
    description: |
      Binds a name to a value matched by a nested pattern.
      Simple variable binding is AsPattern with WildcardPattern nested.
      Items: [tag, attributes, nestedPattern, variableName]
  
  TuplePattern:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "tuple_pattern"
      - $ref: "#/definitions/Attributes"
      - type: array
        items:
          $ref: "#/definitions/Pattern"
        description: "Patterns for each tuple element."
    description: "Matches a tuple by matching each element."
  
  ConstructorPattern:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "constructor_pattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/FQName"
      - type: array
        items:
          $ref: "#/definitions/Pattern"
        description: "Patterns for constructor arguments."
    description: "Matches a specific type constructor and its arguments."
  
  EmptyListPattern:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "empty_list_pattern"
      - $ref: "#/definitions/Attributes"
    description: "Matches an empty list (the [] pattern)."
  
  HeadTailPattern:
    type: array
    minItems: 4
    maxItems: 4
    items:
      - const: "head_tail_pattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Pattern"
      - $ref: "#/definitions/Pattern"
    description: |
      Matches a non-empty list by head and tail (the x :: xs pattern).
      Items: [tag, attributes, headPattern, tailPattern]
  
  LiteralPattern:
    type: array
    minItems: 3
    maxItems: 3
    items:
      - const: "literal_pattern"
      - $ref: "#/definitions/Attributes"
      - $ref: "#/definitions/Literal"
    description: "Matches an exact literal value."
  
  UnitPattern:
    type: array
    minItems: 2
    maxItems: 2
    items:
      - const: "unit_pattern"
      - $ref: "#/definitions/Attributes"
    description: "Matches the unit value."
  
  # ===== Value Specifications and Definitions =====
  
  ValueSpecification:
    type: object
    required: ["inputs", "output"]
    properties:
      inputs:
        type: array
        items:
          type: array
          minItems: 2
          maxItems: 2
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Type"
        description: "Function parameters as (name, type) pairs."
      output:
        $ref: "#/definitions/Type"
        description: "The return type."
    description: |
      The type signature of a value or function.
      Contains only type information, no implementation.
  
  ValueDefinition:
    type: object
    required: ["inputTypes", "outputType", "body"]
    properties:
      inputTypes:
        type: array
        items:
          type: array
          minItems: 3
          maxItems: 3
          items:
            - $ref: "#/definitions/Name"
            - $ref: "#/definitions/Attributes"
            - $ref: "#/definitions/Type"
        description: "Function parameters as (name, attributes, type) tuples."
      outputType:
        $ref: "#/definitions/Type"
        description: "The return type."
      body:
        $ref: "#/definitions/Value"
        description: "The value expression implementing the logic."
    description: |
      The complete implementation of a value or function.
      Contains both type information and implementation.

5 - Frontends

Frontends compile source code in a programming language into a Morphir IR model.

A frontend reads source documents in one programming language and writes a Morphir IR model. A backend does the opposite: it reads a model and generates code.

LanguageFrontendStatus
F#F# frontendIn process in dotnet-morphir compile, and as the MEP guest morphir-fsharp-frontend for morphir compile --extension. See compile.

5.1 - F# frontend

Compile a subset of F# into a Morphir IR model.

The F# frontend compiles F# source documents into a Morphir IR model. It writes IR versions 1, 2 and 3 (the classic format) and version 4. It uses the F# compiler service to parse and type-check the source, so a model has the types that F# infers.

The frontend accepts a subset of F#: immutable data and pure functions. It refuses every other construct with an error diagnostic. The coverage reference lists each refused construct, with its code and an example.

Status

The frontend has two forms:

  • In process. Morphir.Frontend.FSharp.FSharpFrontend.registration is an IFrontend from Morphir.Compilation. The dotnet-morphir tool uses it.
  • MEP extension. morphir-fsharp-frontend is a Morphir Extension Protocol (MEP) 0.2.0-draft.1 guest. Hosts that cannot load the F# compiler service, such as the AOT morphir executable, start it as a process. See Run the frontend as a MEP extension.

To compile F# from the command line, see compile.

Input

A compilation gets:

  • a package name, such as acme/car-rental;
  • the modules that the package exposes;
  • the source documents, in compilation order, as F# requires.

Each F# module, nested modules included, becomes one Morphir module. The module path is the namespace and module names without the package path.

namespace Acme.CarRental

module Pricing =
    type Car = { Plate: string; Seats: int }

    type Status =
        | Available
        | Rented of days: int

    let dailyRate (car: Car) : int = if car.Seats > 4 then 60 else 40

    let charge (car: Car) (status: Status) : int =
        match status with
        | Available -> 0
        | Rented days -> dailyRate car * days

Supported subset

AreaSupported
TypesType abbreviations, records, unions with named or positional fields, generic parameters, tuples, functions, unit, bool, int, int64, float, decimal, char, string, option, Result and list
ValuesLiterals, variables, references, constructor application, tuples, lists, record construction, field access, record update, application, lambdas, let, let rec and let rec ... and, if/then/else, and match
PatternsWildcard, variable, literal, tuple, constructor, empty list, cons, as and unit, in match and in function and let parameters
Operators and library functionsArithmetic, comparison, equality, Boolean operators, |>, <|, >> and <<, and mapped functions of List, Option, Result and String

The frontend keeps the order of fields, constructor arguments and match cases, and the currying and evaluation structure of the source. A pipe or a composition becomes a direct application, not a function in the model.

Operators and library functions map to Morphir SDK operations through an explicit table. The type of an operand selects the operation: a + b on decimal values is SDK Decimal.add. A reference with no entry in the table gives fsharp.unsupported_reference and names the F# symbol.

Names and access

F# names become Morphir names: TaxRate and taxRate are both tax rate. Two declarations in one scope with the same Morphir name give fsharp.name_collision. To give a declaration a different Morphir name, use the [<MorphirName("...")>] attribute from the Morphir.FSharp.Annotations package (namespace Morphir). The name is in canonical Morphir syntax.

A module is public when the list of exposed modules names it, or when the compilation has no such list. An empty list exposes no module. An exposed module that matches no compiled module gives compile.unknown_exposed_module. A private or internal declaration is private.

Diagnostics

Diagnostics use the MEP diagnostic shape: a severity, a stable code, a message and a location. Positions are zero-based, and characters count UTF-16 code units. An error stops the compilation, and the frontend writes no model. Warnings and information diagnostics come with a successful model.

F# compiler errors have the code fsharp.compiler. Refused constructs have the codes in the coverage reference. When the frontend cannot give the compiler its references, for example in a self-contained folder whose deps file is missing or unreadable, compilation fails with one fsharp.references_unavailable error that names the cause, and the compiler does not run.

Run the frontend as a MEP extension

Install the guest as a dotnet tool. It needs the .NET runtime.

dotnet tool install --global Morphir.Frontend.FSharp.Extension

The same guest also has a self-contained folder delivery that carries its own .NET runtime, for machines without .NET. See Compile without .NET installed. Both deliveries compile with the same references (the .NET framework, FSharp.Core and Morphir.FSharp.Annotations), so at the same .NET runtime version they give the same IR and diagnostics. The model revision names the frontend identity, which includes the runtime version, so it is equal only on the same runtime patch.

The guest speaks only MEP 0.2.0-draft.1, over stdio with Content-Length framing. It serves morphir.frontend.compileModel and claims:

ClaimValue
Claims version0.1.0-draft.2
Languagefsharp, with the file extension .fs
Several documents in one requestyes (multiDocument: true)
Legacy morphir.frontend.compileno
Incremental compilationno
Model tuplesmorphir.frontend.compileModel, profile morphir-ir, codec morphir-json, envelope 0.1.0-draft.1, IR 4.0.0 and 3.0.0

Classic IR versions 1 and 2 have no tuple. For them, the host compiles at 3.0.0, reads the classic model, and writes it at the requested version. The model does not change family. A host that agrees only 4.0.0 with a guest refuses a classic request before it sends one.

To use the guest from the morphir executable, give its path with --extension, or set it in the project configuration:

morphir compile --extension "$(which morphir-fsharp-frontend)"
[frontend.fsharp]
extension = "morphir-fsharp-frontend"

The host starts the guest with explicit arguments and no shell, and passes the guest’s stderr to its own stderr. Before it sends a request, it checks that the session admits the language, the number of documents and the IR version. It always ends the session with shutdown and exit, or stops the process after a deadline. Guest output on stdout that is not a frame fails the compile with extension.protocol_error.

The model in a successful result is a verified envelope: canonical base64 of the IR JSON, its exact length and its SHA-256. The host checks them before it reads the IR. For the same documents, the IR and the diagnostics from the guest equal the in-process results; a test compares them for every case of the conformance corpus.

Upstream qualification: the MEP guest kit of finos/morphir (generic-json-library-v1) still needs a legacy 0.1 stage and a backend, so the guest is not qualified with it yet. Its conformance comes from the pinned 0.2.0-draft.1 contract, the pinned session transcripts and the model-transport cases. See deviations.

How the frontend is checked

  • The conformance corpus has cases for IR versions 1 to 4 and three input kinds. Each refused construct has its own case. The cases do not depend on the implementation.
  • Behavior cases call the compiled values with JSON arguments and compare the results.
  • A round trip generates F# from each compiled model with the F# backend, compiles that F# again, and requires the same model.

Prior art

The frontend uses the design of two F# projects, but it does not depend on them.

ProjectWhat the frontend usesWhat it does not use, and why
Fable (MIT)Design patterns: how decision trees appear in the typed tree, a replacement table for core-library calls, how inline values and witnesses appear in the typed tree, and a resolver interface for project cracking.Fable.Transforms as a library. It carries Fable’s own syntax tree and replacements for JavaScript, Python and Rust targets.
Fantomas (Apache-2.0)The F# backend formats generated code with Fantomas.Core. The round-trip check uses that backend.Fantomas.FCS in the frontend. Its syntax tree is not binary-compatible with the F# compiler service, and the frontend needs the compiler service for type checking. A second parser would give a second set of ranges.

If code is adapted from these projects, and not only its design, the source file names its origin and the NOTICE file gives the attribution.

5.1.1 - F# frontend coverage

The F# constructs that the F# frontend refuses, with their diagnostic codes and examples.

The F# frontend compiles a subset of F#. Before it lowers a document, admission checks the syntax and refuses each construct outside the subset with an error diagnostic. All refusals of a run are reported together, and the frontend writes no model. The overview describes the supported subset.

This page is generated from the admission catalog of the frontend, so it lists every code that admission can report. Each rule has a minimal example that gives only its own code. The conformance corpus has one refusal case for each code. Codes are stable once released.

Codes

CodeConstruct
fsharp.unsupported_mutablemutable binding or field
fsharp.unsupported_assignmentassignment
fsharp.unsupported_looploop
fsharp.unsupported_classclass
fsharp.unsupported_interfaceinterface
fsharp.unsupported_membermember
fsharp.unsupported_object_expressionobject expression
fsharp.unsupported_sequence_expressionsequence expression
fsharp.unsupported_computation_expressioncomputation expression
fsharp.unsupported_comprehensionlist comprehension or range
fsharp.unsupported_arrayarray
fsharp.unsupported_exceptionexception
fsharp.unsupported_active_patternactive pattern
fsharp.unsupported_unit_of_measureunit of measure
fsharp.unsupported_quotationquotation
fsharp.unsupported_anonymous_recordanonymous record
fsharp.unsupported_structstruct type or struct tuple
fsharp.unsupported_inlineinline function
fsharp.unsupported_attributeattribute with an effect
fsharp.unsupported_type_testtype test
fsharp.unsupported_castcast
fsharp.unsupported_nullnull
fsharp.unsupported_useuse binding
fsharp.unsupported_sequentialsequential expression
fsharp.unsupported_literalliteral of an unsupported type
fsharp.unsupported_enumenum
fsharp.unsupported_delegatedelegate
fsharp.unsupported_pattern_guardpattern guard
fsharp.unsupported_or_patternor pattern
fsharp.unsupported_record_patternrecord pattern
fsharp.unsupported_list_patternlist pattern with elements
fsharp.unsupported_named_field_patternnamed field pattern
fsharp.unsupported_conditional_compilationconditional compilation
fsharp.unsupported_signature_filesignature file

The fallback rules below refuse any expression, pattern, type or declaration outside the supported subset that no specific rule refused.

CodeConstruct
fsharp.unsupported_expressionexpression outside the supported subset
fsharp.unsupported_patternpattern outside the supported subset
fsharp.unsupported_typetype outside the supported subset
fsharp.unsupported_declarationdeclaration outside the supported subset

Rules

fsharp.unsupported_mutable

Construct: mutable binding or field

Message: Mutable bindings and fields are not supported; Morphir values are immutable.

Suggestion: Use a new let binding, or a record update, instead of changing a value.

Example. The diagnostic is at mutable.

Example.fs:

namespace Acme.Demo
module Example =
    let mutable total = 0

fsharp.unsupported_assignment

Construct: assignment

Message: Assignment with ‘<-’ is not supported; Morphir values are immutable.

Suggestion: Return a new value instead of changing an existing one.

Example. The diagnostic is at builder.Length <- 0.

Example.fs:

namespace Acme.Demo
module Example =
    let reset (builder: System.Text.StringBuilder) = builder.Length <- 0

fsharp.unsupported_loop

Construct: loop

Message: Loops (‘for’, ‘while’) are not supported; Morphir has no statements.

Suggestion: Use recursion or a List function such as List.map or List.fold.

Example. The diagnostic is at for index in 1 .. 3 do ignore index.

Example.fs:

namespace Acme.Demo
module Example =
    let count () = for index in 1 .. 3 do ignore index

fsharp.unsupported_class

Construct: class

Message: Classes are not supported; Morphir types are records, unions and type abbreviations.

Suggestion: Use a record for the data and module functions for the behavior.

Example. The diagnostic is at this text:

Counter() =
        member _.Value = 1

Example.fs:

namespace Acme.Demo
module Example =
    type Counter() =
        member _.Value = 1

fsharp.unsupported_interface

Construct: interface

Message: Interfaces are not supported; Morphir has no subtyping.

Suggestion: Use a record of functions, or a union of the cases.

Example. The diagnostic is at this text:

IShape =
        abstract Area: float

Example.fs:

namespace Acme.Demo
module Example =
    type IShape =
        abstract Area: float

fsharp.unsupported_member

Construct: member

Message: Members and type extensions are not supported; Morphir types have no methods.

Suggestion: Define a module function that takes the value as a parameter.

Example. The diagnostic is at member this.Double = this.Seats * 2.

Example.fs:

namespace Acme.Demo
module Example =
    type Car = { Seats: int } with
        member this.Double = this.Seats * 2

fsharp.unsupported_object_expression

Construct: object expression

Message: Object expressions are not supported; Morphir has no objects.

Suggestion: Use a record of functions.

Example. The diagnostic is at { new System.Object() with member _.ToString() = "x" }.

Example.fs:

namespace Acme.Demo
module Example =
    let text = { new System.Object() with member _.ToString() = "x" }

fsharp.unsupported_sequence_expression

Construct: sequence expression

Message: Sequence expressions (‘seq { }’) are not supported; Morphir has no lazy sequences.

Suggestion: Use a list and the List functions.

Example. The diagnostic is at { yield 1 }.

Example.fs:

namespace Acme.Demo
module Example =
    let numbers = seq { yield 1 }

fsharp.unsupported_computation_expression

Construct: computation expression

Message: Computation expressions are not supported; Morphir has no effects or builders.

Suggestion: Write the steps as plain functions, for example with Result.bind.

Example. The diagnostic is at { return 1 }.

Example.fs:

namespace Acme.Demo
module Example =
    let work = async { return 1 }

fsharp.unsupported_comprehension

Construct: list comprehension or range

Message: List comprehensions and ranges (’[ for … ]’, ‘[ 1 .. 3 ]’) are not supported.

Suggestion: Use List.map, List.filter or an explicit list.

Example. The diagnostic is at [ for x in [ 1; 2 ] -> x * x ].

Example.fs:

namespace Acme.Demo
module Example =
    let squares = [ for x in [ 1; 2 ] -> x * x ]

fsharp.unsupported_array

Construct: array

Message: Arrays are not supported; Morphir collections are immutable lists.

Suggestion: Use a list.

Example. The diagnostic is at [| 1; 2 |].

Example.fs:

namespace Acme.Demo
module Example =
    let numbers = [| 1; 2 |]

fsharp.unsupported_exception

Construct: exception

Message: Exceptions (’exception’, ’try … with’, ’try … finally’) are not supported; Morphir has no effects.

Suggestion: Return a Result or an option.

Example. The diagnostic is at exception Failed of string.

Example.fs:

namespace Acme.Demo
module Example =
    exception Failed of string

fsharp.unsupported_active_pattern

Construct: active pattern

Message: Active patterns are not supported; Morphir patterns match only constructors and values.

Suggestion: Use a union and a function that returns it.

Example. The diagnostic is at let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd.

Example.fs:

namespace Acme.Demo
module Example =
    let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd

fsharp.unsupported_unit_of_measure

Construct: unit of measure

Message: Units of measure are not supported; Morphir numbers carry no unit.

Suggestion: Use a single-case union to name the quantity.

Example. The diagnostic is at [<Measure>] type kg.

Example.fs:

namespace Acme.Demo
module Example =
    [<Measure>] type kg

fsharp.unsupported_quotation

Construct: quotation

Message: Quotations are not supported; Morphir models values, not code.

Example. The diagnostic is at <@ 1 + 1 @>.

Example.fs:

namespace Acme.Demo
module Example =
    let expression = <@ 1 + 1 @>

fsharp.unsupported_anonymous_record

Construct: anonymous record

Message: Anonymous records are not supported; Morphir record types have names.

Suggestion: Declare a record type.

Example. The diagnostic is at {| X = 1 |}.

Example.fs:

namespace Acme.Demo
module Example =
    let point = {| X = 1 |}

fsharp.unsupported_struct

Construct: struct type or struct tuple

Message: Struct types and struct tuples are not supported; Morphir has no value-type layout.

Suggestion: Remove [] or ‘struct’.

Example. The diagnostic is at struct (1, 2).

Example.fs:

namespace Acme.Demo
module Example =
    let pair = struct (1, 2)

fsharp.unsupported_inline

Construct: inline function

Message: Inline functions are not supported; Morphir functions are not specialized at their call sites.

Suggestion: Remove ‘inline’ and give the parameters concrete types.

Example. The diagnostic is at inline.

Example.fs:

namespace Acme.Demo
module Example =
    let inline double (x: int) = x * 2

fsharp.unsupported_attribute

Construct: attribute with an effect

Message: This attribute changes the meaning of the code and is not supported.

Suggestion: Remove the attribute. Only RequireQualifiedAccess, AutoOpen, CompilationRepresentation, CompiledName and MorphirName are accepted.

Example. The diagnostic is at Literal.

Example.fs:

namespace Acme.Demo
module Example =
    [<Literal>]
    let Limit = 10

fsharp.unsupported_type_test

Construct: type test

Message: Type tests (’:?’) are not supported; Morphir has no runtime types.

Suggestion: Use a union to tell the cases apart.

Example. The diagnostic is at value :? string.

Example.fs:

namespace Acme.Demo
module Example =
    let isText (value: obj) = value :? string

fsharp.unsupported_cast

Construct: cast

Message: Casts (’:>’, ‘:?>’, ‘upcast’, ‘downcast’) are not supported; Morphir has no subtyping.

Example. The diagnostic is at 1 :> obj.

Example.fs:

namespace Acme.Demo
module Example =
    let boxed = (1 :> obj)

fsharp.unsupported_null

Construct: null

Message: ’null’ is not supported; Morphir values are never null.

Suggestion: Use an option.

Example. The diagnostic is at null.

Example.fs:

namespace Acme.Demo
module Example =
    let nothing: string = null

fsharp.unsupported_use

Construct: use binding

Message: ‘use’ bindings are not supported; Morphir has no resources to dispose.

Suggestion: Use ’let’.

Example. The diagnostic is at this text:

use stream = new System.IO.MemoryStream()
        stream.Length

Example.fs:

namespace Acme.Demo
module Example =
    let size () =
        use stream = new System.IO.MemoryStream()
        stream.Length

fsharp.unsupported_sequential

Construct: sequential expression

Message: Sequential expressions (‘a; b’) are not supported; Morphir values have no side effects.

Suggestion: Remove the expression whose value is discarded.

Example. The diagnostic is at this text:

ignore 1
        2

Example.fs:

namespace Acme.Demo
module Example =
    let both () =
        ignore 1
        2

fsharp.unsupported_literal

Construct: literal of an unsupported type

Message: This literal type is not supported; Morphir numbers are Int, Float and Decimal.

Suggestion: Use an int, int64, float or decimal literal.

Example. The diagnostic is at 1uy.

Example.fs:

namespace Acme.Demo
module Example =
    let small = 1uy

fsharp.unsupported_enum

Construct: enum

Message: Enums are not supported; Morphir custom types have no numeric values.

Suggestion: Use a union without fields.

Example. The diagnostic is at this text:

Color =
        | Red = 0
        | Blue = 1

Example.fs:

namespace Acme.Demo
module Example =
    type Color =
        | Red = 0
        | Blue = 1

fsharp.unsupported_delegate

Construct: delegate

Message: Delegates are not supported; Morphir has function types.

Suggestion: Use a function type.

Example. The diagnostic is at Handler = delegate of int -> unit.

Example.fs:

namespace Acme.Demo
module Example =
    type Handler = delegate of int -> unit

fsharp.unsupported_pattern_guard

Construct: pattern guard

Message: Pattern guards (‘when’) are not supported; a Morphir pattern has no condition.

Suggestion: Match the case, then use ‘if’ in its body.

Example. The diagnostic is at n > 3.

Example.fs:

namespace Acme.Demo
module Example =
    let size n =
        match n with
        | n when n > 3 -> "big"
        | _ -> "small"

fsharp.unsupported_or_pattern

Construct: or pattern

Message: Or patterns (‘A | B’) are not supported; a Morphir case has one pattern.

Suggestion: Write one case for each alternative.

Example. The diagnostic is at 1 | 2.

Example.fs:

namespace Acme.Demo
module Example =
    let low n =
        match n with
        | 1 | 2 -> true
        | _ -> false

fsharp.unsupported_record_pattern

Construct: record pattern

Message: Record patterns are not supported.

Suggestion: Bind the record to a variable and read its fields.

Example. The diagnostic is at { Seats = s }.

Example.fs:

namespace Acme.Demo
module Example =
    type Car = { Seats: int }
    let seats car =
        match car with
        | { Seats = s } -> s

fsharp.unsupported_list_pattern

Construct: list pattern with elements

Message: List patterns with elements (’[ a; b ]’) are not supported.

Suggestion: Use ‘::’ and ‘[]’ patterns.

Example. The diagnostic is at [ a ].

Example.fs:

namespace Acme.Demo
module Example =
    let first xs =
        match xs with
        | [ a ] -> a
        | _ -> 0

fsharp.unsupported_named_field_pattern

Construct: named field pattern

Message: Named field patterns (‘Case(field = p)’) are not supported.

Suggestion: Match the fields by position.

Example. The diagnostic is at Rented(days = d).

Example.fs:

namespace Acme.Demo
module Example =
    type Status = Rented of days: int
    let days status =
        match status with
        | Rented(days = d) -> d

fsharp.unsupported_conditional_compilation

Construct: conditional compilation

Message: Conditional compilation (’#if’) is not supported, because this frontend does not choose defines.

Suggestion: Remove the directive, or keep one branch.

Example. The diagnostic is at #if DEBUG.

Example.fs:

namespace Acme.Demo
module Example =
#if DEBUG
    let mode = "debug"
#else
    let mode = "release"
#endif

fsharp.unsupported_signature_file

Construct: signature file

Message: Signature files (’.fsi’) are not supported; access comes from F# visibility and the exposed modules.

Suggestion: Remove the signature file and use ‘private’ or ‘internal’.

Example. The diagnostic is an empty range at the start of the first document.

Example.fsi:

namespace Acme.Demo
module Example =
    val total: int

Example.fs:

namespace Acme.Demo
module Example =
    let total = 1

fsharp.unsupported_expression

Construct: expression outside the supported subset

Message: This kind of expression is not supported by the F# frontend.

Example. The diagnostic is at lazy 1.

Example.fs:

namespace Acme.Demo
module Example =
    let later = lazy 1

fsharp.unsupported_pattern

Construct: pattern outside the supported subset

Message: This kind of pattern is not supported by the F# frontend.

Example. The diagnostic is at (a, _) & (_, b).

Example.fs:

namespace Acme.Demo
module Example =
    let both pair =
        match pair with
        | (a, _) & (_, b) -> a + b

fsharp.unsupported_type

Construct: type outside the supported subset

Message: This kind of type is not supported by the F# frontend.

Example. The diagnostic is at #seq<int>.

Example.fs:

namespace Acme.Demo
module Example =
    let total (values: #seq<int>) = Seq.sum values

fsharp.unsupported_declaration

Construct: declaration outside the supported subset

Message: This kind of declaration is not supported by the F# frontend.

Example. The diagnostic is at do ignore 1.

Example.fs:

namespace Acme.Demo
module Example =
    do ignore 1

6 - API Reference

Complete API reference for Morphir .NET

Overview

The Morphir .NET API provides comprehensive support for working with Morphir IR (Intermediate Representation).

Core Namespaces

Morphir.Core

The core namespace contains the fundamental types and structures for Morphir IR:

  • IR: Intermediate representation types
  • Types: Type expression models
  • Values: Value expression models
  • Names: Name and path handling

Morphir

The main namespace contains CLI and application-level functionality:

  • CLI: Command-line interface
  • Serialization: JSON serialization support

Getting Started

To use Morphir .NET in your project:

using Morphir.Core.IR;
using Morphir.Core.Types;

// Create a type expression
var intType = new TypeExpr.TInt();

// Create a function type
var funcType = new TypeExpr.TFunc(
    new TypeExpr.TInt(),
    new TypeExpr.TString()
);

Documentation

For detailed API documentation, see the generated XML documentation comments in the source code, or explore the source directly:

6.1 - Compilation contracts

Exact IR versions, checked diagnostics, native IR builders and IR writers in Morphir.Compilation.

Morphir.Compilation holds the language-neutral parts of morphir compile:

  • exact IR format versions;
  • diagnostics that travel on success and on failure;
  • the IR builder interface, which a frontend lowers against once;
  • the writers that turn a built model into JSON for one exact version;
  • the in-process compile contract that a frontend implements.

The design is in the add-fsharp-compile design (decisions VER-1, DIAG-1, MID-1, IRW-1 and FE-1).

Format versions

A format version is never an integer. It is a Versioning.FormatVersion: an integer classic version, or a SemVer version that can have pre-release and build parts. FormatVersion.sameVersion is the only equivalence rule:

LeftRightSame version
33.0.0yes
44.0.0+build.7yes: build metadata has no effect
4.1.0-draft.14.1.0-draft.1yes
4.1.0-draft.14.1.0no: a pre-release never matches its release
4.1.0-draft.14.1.0-draft.2no

IrTarget equality and hashing follow the same rule. A writer writes the integer n for n and n.0.0, and the exact text for any other version (FormatVersion.canonicalText).

Checked results

Every stage returns Checked<'T>, never Result<'T, Diagnostic>:

  • a passed result has a value and every warning, info and hint gathered for it;
  • a failed result has at least one error, and every diagnostic gathered before it.

Only the severity decides the outcome. In the check { } builder, let! runs dependent steps and stops at the first failure; and! and Checked.traverse run independent steps and keep the diagnostics of all of them. The builder is named check because checked is a reserved F# keyword. C# callers use Checked<T>.Pass, Checked<T>.Fail, TryGetValue and Match.

Diagnostics have the MEP shape: a severity, an optional parsed DiagnosticCode, a message, an optional Location (zero-based lines, characters in UTF-16 code units, exclusive end), related locations and data.

IR builders

A frontend writes its lowering once, as an IBuilderProgram, against IIrBuilder<…>. That interface has one member for each IR node of the supported subset. Each family implements it natively:

BuilderBuildsNotes
ClassicBuilderclassic IR, for format versions 1, 2 and 3Each value and pattern carries its inferred type as its attribute. Classic IR has no source locations. An integer outside int64, a character outside the Basic Multilingual Plane and a non-finite float are refused with morphir.unrepresentable.
V4BuilderIR v4Each value and pattern carries its inferred type and its 1-based source; types carry their source. Record fields, constructors and let-recursion definitions keep source order.

No code converts a model from one family to another. Whole-program work (names, access, exposed modules, declaration order) happens in the frontend before building, so it is the same for every family. The builder contracts are in the Morphir.Compilation.Building namespace and are for F# code only.

Adding a family

  1. Implement IIrBuilder<…> with the new family’s own node types.
  2. Add a case to FamilyModel for the family’s model.
  3. Implement IFamilyBuilder, and register it in the FamilyBuilderRegistry.
  4. Add a writer (below) for each format version of the family.

No frontend changes. The compiler makes every builder implement every member.

IR writers

An IIrWriter declares the exact versions it writes. WriterRegistry.Resolve selects the one writer whose declared versions contain the requested version under sameVersion. It refuses a version that no writer declares (compile.unsupported_ir_version) and a version that two writers declare (compile.ambiguous_ir_writer). It never picks the nearest version. A new version, including a pre-release, is one new writer declaration.

WriterDeclaresWrites
ClassicIrWriter1, 2, 3Classic library JSON through ClassicJsonWriter, with the wire profile of the version.
V4IrWriter4Canonical compact IR v4 JSON through V4JsonWriter, with number lexemes taken from the model.

The classic writer reads its rules from a ClassicWireProfile, a record of data with no behavior:

Rulev1v2v3
Type tagssnake_casePascalCasePascalCase
Value, pattern and literal tagssnake_casesnake_casePascalCase
Access wrapper["public", x]{"access": "Public", "value": x}{"access": "Public", "value": x}
Module list[{"name": path, "def": module}][[path, module]][[path, module]]
Library taglibraryLibraryLibrary
Module without documentationdoc left outdoc left out"doc": null

WriteOptions sets the layout (Indented) and the spelling of a type or value without documentation (AbsentDocumentation). WriteOptions.Compact is compact and uses Omit: the member is written without a documentation wrapper, which gives smaller output. EmptyWrapper writes {"doc": "", "value": ...}, as morphir-elm does, in all three classic versions. The reader accepts both forms. IR v4 always leaves out absent documentation. C# callers change one setting with WithIndented and WithAbsentDocumentation.

Frontends

A frontend implements IFrontend. It has an identity, a transport and a FrontendCapability: the languages and file extensions it reads, the IR targets it produces, and whether one request can hold more than one document. CompileAsync takes a CompileRequest and returns a FrontendOutcome:

OutcomeMeaningExit code in the CLI
Completed with a passed resultA CompiledPackage: the model in the requested family and the module names. Warnings, information and hints travel with it.0
Completed with a failed resultSource problems, each a diagnostic with a code and a range.1
FailedAn operational failure, such as a language or target that the frontend does not serve. It keeps the diagnostics gathered before it.2

Only CompileRequest.Parse, TryParse and Create build a request. They refuse a missing language, a missing package name, an empty document list, a missing document, a document without a URI or text, a document in another language and a repeated URI. A language is a parsed LanguageId, for example fsharp: a lowercase letter, then lowercase letters, digits, -, _ and .. The exposed modules are absent to expose every public module, or a list (which can be empty) to expose only those modules.

The in-process F# frontend is FSharpFrontend.registration in Morphir.Frontend.FSharp. It produces every target for which a builder and a writer are registered: classic versions 1, 2 and 3, and 4. The conformance runner’s FrontendAdapter.create runs the Morphir Compatibility Kit against any IFrontend.

Example

This example builds one module with both families and writes classic format version 3.0.0. A test compiles and runs the same code.

open System.Numerics
open Morphir.Compilation
open Morphir.Compilation.Building
open Morphir.IR
open Morphir.IR.Model

let here =
    Location.Create("Main.fs", Range.Create(Position.Create(0, 0), Position.Create(0, 4)))

let name (text: string) = Name.fromString text

let path (texts: string list) =
    Path.fromList (
        texts
        |> List.map name
    )

/// One lowering, written once against any family's builder: Main.rate = 60.
let program =
    { new IBuilderProgram with
        member _.Run(builder) = check {
            let intType: FQName = {
                PackagePath = PackageName.packageName (path [ "morphir"; "s-d-k" ])
                ModulePath = ModulePath.modulePath (path [ "basics" ])
                LocalName = name "int"
            }

            let! int = builder.TypeReference(here, intType, [])
            let! sixty = builder.Literal(here, int, LiteralValue.Integer(BigInteger 60))
            let! rate = builder.ValueDefinition(here, [], int, sixty)

            let plan = {
                Path = ModulePath.modulePath (path [ "main" ])
                Access = Access.Public
                Documentation = None
                Location = here
            }

            let rateMember = {
                Name = name "rate"
                Access = Access.Public
                Documentation = Some "The daily rate."
                Definition = rate
                Location = here
            }

            let! main = builder.Module(plan, [], [ rateMember ])

            return!
                builder.Package(
                    {
                        Package = PackageName.packageName (path [ "acme" ])
                        Documentation = None
                    },
                    [ main ]
                )
        }
    }

let builders =
    FamilyBuilderRegistry.Create [ ClassicFamilyBuilder() :> IFamilyBuilder; V4FamilyBuilder() ]

let writers = WriterRegistry.Create [ ClassicIrWriter() :> IIrWriter; V4IrWriter() ]

let written = check {
    let! resolved =
        writers.Resolve(
            Versioning.SemVer
                {
                    Major = 3
                    Minor = 0
                    Patch = 0
                    PreRelease = None
                    BuildMetadata = None
                }
        )

    let! builder = builders.Resolve resolved.Target.Family
    let! model = builder.Build program
    return! resolved.Writer.Write(resolved.Target, WriteOptions.Compact, model)
}

// written passes with the bytes of {"formatVersion":3,"distribution":["Library",[["acme"]],[],...]}

7 - CLI Reference

Command-line interface reference for Morphir .NET tooling

Morphir .NET CLI Reference

The Morphir .NET CLI provides powerful command-line tools for working with Morphir IR files, validating schemas, and managing Morphir projects.

Available Commands

Toolchain manager

  • morphirup - The standalone toolchain manager. This build makes plan-only setups and checks a project’s request and lock; it acquires and installs nothing yet

IR Management

  • morphir ir verify - Validate Morphir IR JSON files against official schemas
  • morphir ir detect-version (coming in Phase 2) - Detect the schema version of an IR file

Project Management

These migration commands are not yet implemented.

Installation

The Morphir .NET CLI is distributed as a .NET tool. Install it globally with:

dotnet tool install -g Morphir.CLI

Or locally in a project:

dotnet tool install Morphir.CLI

Getting Help

For help with any command, use the --help flag:

morphir --help
morphir ir --help
morphir ir verify --help

Common Workflows

Validating IR Files

The most common workflow is validating Morphir IR JSON files to ensure they conform to the expected schema:

# Validate a single file with auto-detection
morphir ir verify path/to/morphir-ir.json

# Validate with explicit schema version
morphir ir verify --schema-version 3 path/to/morphir-ir.json

# Get JSON output for CI/CD
morphir ir verify --json path/to/morphir-ir.json

# Quiet mode (only errors)
morphir ir verify --quiet path/to/morphir-ir.json

See the morphir ir verify documentation for complete details.

Exit Codes

All Morphir CLI commands use consistent exit codes:

  • 0: Success - operation completed successfully
  • 1: Validation failure - IR file failed schema validation
  • 2: Operational error - file not found, invalid JSON, missing dependencies, etc.

These exit codes are designed for CI/CD integration and scripting.

Configuration

The configuration guide distinguishes the compatibility TOML cache loader from the additive shared configuration and project system.

The morphir config read commands inspect effective/scoped values, source reports and redacted explanations. JSON output follows the published schema; success, missing keys and usage/loading failures use exit codes 0/1/2. Shared config set/unset editing, project migrate and bounded workspace migrate are available. Start with the workspace configuration walkthrough.

Troubleshooting

See the Troubleshooting Guide for solutions to common issues.

7.1 - morphir ir verify

Validate Morphir IR JSON files against official schemas

morphir ir verify

Validate Morphir IR JSON files against the official JSON schemas for format versions 1, 2, and 3.

Synopsis

morphir ir verify <file-path> [options]

Description

The morphir ir verify command validates a Morphir IR JSON file against the appropriate schema specification. The command automatically detects the schema version from the file content, or you can explicitly specify the version to use.

This command is useful for:

  • Catching structural errors before IR files are used by other tools
  • Validating generated IR from Morphir compilers
  • CI/CD integration to ensure IR quality
  • Debugging IR issues with detailed error messages

Arguments

<file-path>

Required. Path to the Morphir IR JSON file to validate.

  • Supports absolute and relative paths
  • File must exist and be readable
  • File must contain valid JSON

Examples:

morphir ir verify morphir-ir.json
morphir ir verify ../output/morphir-ir.json
morphir ir verify /absolute/path/to/morphir-ir.json

Options

--schema-version <version>

Explicitly specify the schema version to validate against.

  • Valid values: 1, 2, or 3
  • Default: Auto-detected from file content
  • Use when: Testing version-specific compatibility or overriding auto-detection

Examples:

# Validate against v3 schema regardless of file content
morphir ir verify --schema-version 3 morphir-ir.json

# Test if a v2 IR file is compatible with v3 schema
morphir ir verify --schema-version 3 morphir-ir-v2.json

--json

Output validation results in JSON format instead of human-readable text.

  • Default: Human-readable output
  • Use when: Parsing results in CI/CD, scripts, or other tools
  • Output: Structured JSON with validation details

JSON Output Format:

{
  "IsValid": true,
  "SchemaVersion": "3",
  "DetectionMethod": "auto",
  "FilePath": "/path/to/morphir-ir.json",
  "Errors": [],
  "Timestamp": "2025-12-15T10:30:00Z"
}

For invalid IR:

{
  "IsValid": false,
  "SchemaVersion": "3",
  "DetectionMethod": "auto",
  "FilePath": "/path/to/morphir-ir.json",
  "Errors": [
    {
      "Path": "$.distribution[3]",
      "Message": "Required properties [\"formatVersion\"] are not present",
      "Expected": "required property",
      "Found": "undefined (missing)",
      "Line": null,
      "Column": null
    }
  ],
  "Timestamp": "2025-12-15T10:30:00Z"
}

Example:

# Get JSON output for parsing
morphir ir verify --json morphir-ir.json

# Use in scripts
RESULT=$(morphir ir verify --json morphir-ir.json)
echo $RESULT | jq '.IsValid'

--quiet

Suppress all output except for errors.

  • Default: Show detailed output
  • Use when: Running in CI/CD pipelines where you only care about failures
  • Exit code: Still returns 0 (success) or 1 (failure)

Examples:

# Quiet mode - only shows output if validation fails
morphir ir verify --quiet morphir-ir.json

# Use in CI/CD
if morphir ir verify --quiet morphir-ir.json; then
  echo "IR is valid"
else
  echo "IR validation failed"
  exit 1
fi

Exit Codes

CodeMeaningDescription
0SuccessIR file is valid according to the schema
1Validation failureIR file failed schema validation (see error output)
2Operational errorFile not found, malformed JSON, or other operational issue

Output

Human-Readable Output (Default)

Valid IR:

Validation Result: ✓ VALID
File: /path/to/morphir-ir.json
Schema Version: v3 (auto)
Timestamp: 2025-12-15 10:30:00 UTC

No validation errors found.

Invalid IR:

Validation Result: ✗ INVALID
File: /path/to/morphir-ir.json
Schema Version: v3 (auto)
Timestamp: 2025-12-15 10:30:00 UTC

Found 2 validation error(s):

  Path: $.distribution[3]
  Message: Required properties ["formatVersion"] are not present
  Expected: required property
  Found: undefined (missing)

  Path: $.distribution[3].modules[0].name
  Message: Value is "string" but should be "array"
  Expected: array
  Found: string

JSON Output

See the --json option above for the JSON output format specification.

Schema Version Detection

The command automatically detects the schema version by analyzing the IR file structure:

  • v1: Detected by presence of v1-specific structure
  • v2: Detected by "formatVersion": 2 field
  • v3: Detected by "formatVersion": 3 field

Detection Method in Output:

  • auto: Version was automatically detected
  • manual: Version was explicitly specified via --schema-version

Examples

Basic Validation

Validate a single IR file with auto-detection:

morphir ir verify morphir-ir.json

Explicit Version

Validate against a specific schema version:

morphir ir verify --schema-version 3 morphir-ir.json

CI/CD Integration

Validate IR in a GitHub Actions workflow:

- name: Validate Morphir IR
  run: |
    dotnet morphir ir verify --json morphir-ir.json > validation-result.json
    if [ $? -ne 0 ]; then
      cat validation-result.json
      exit 1
    fi

Script with Error Handling

#!/bin/bash
set -e

echo "Validating Morphir IR files..."

for file in output/*.json; do
  echo "Validating $file..."
  if morphir ir verify --quiet "$file"; then
    echo "✓ $file is valid"
  else
    echo "✗ $file is invalid"
    morphir ir verify "$file"  # Show detailed errors
    exit 1
  fi
done

echo "All IR files are valid!"

JSON Output Parsing

Parse JSON output with jq:

# Check if valid
morphir ir verify --json morphir-ir.json | jq '.IsValid'

# Get error count
morphir ir verify --json morphir-ir.json | jq '.Errors | length'

# Extract error messages
morphir ir verify --json morphir-ir.json | jq '.Errors[].Message'

# Get schema version used
morphir ir verify --json morphir-ir.json | jq '.SchemaVersion'

Common Error Messages

File Not Found

Error: File does not exist: path/to/file.json

Solution: Check the file path and ensure the file exists.

Malformed JSON

Validation Result: ✗ INVALID

Found 1 validation error(s):

  Path: $
  Message: Malformed JSON: 'i' is an invalid start of a value. LineNumber: 6 | BytePositionInLine: 4.
  Expected: Valid JSON
  Found: Invalid JSON syntax

Solution: Fix the JSON syntax error. The error message includes the line and byte position.

Missing Required Field

Path: $.distribution
Message: Required properties ["formatVersion"] are not present
Expected: required property
Found: undefined (missing)

Solution: Add the missing formatVersion field to your IR file.

Type Mismatch

Path: $.distribution[3].modules[0].name
Message: Value is "string" but should be "array"
Expected: array
Found: string

Solution: Change the field type to match the schema requirement (in this case, name should be an array, not a string).

Troubleshooting

Schema Version Not Detected

If auto-detection fails or detects the wrong version:

  1. Check the formatVersion field in your JSON (for v2/v3)
  2. Use --schema-version explicitly to override auto-detection
  3. Verify JSON structure matches the expected schema version

Performance Issues

For large IR files (>1MB):

  • Validation may take several seconds
  • Consider using --quiet mode in CI/CD to reduce output overhead
  • (Phase 2 will include performance optimizations for batch processing)

False Positives

If validation fails but you believe the IR is correct:

  1. Check schema version - ensure you’re validating against the correct version
  2. Review error messages - they include expected vs. found values
  3. Consult schema documentation - see Schema Specifications
  4. Report an issue - if you believe the schema or validator has a bug
  • morphir ir detect-version (Phase 2) - Detect schema version without validation
  • morphir ir migrate (Phase 3) - Migrate IR between schema versions

See Also

7.2 - Management Commands

Commands for managing Morphir distributions, tools, and extensions.

Overview

The Morphir CLI provides commands to manage distributions, tools, and extensions with support for:

  • Global-first installation (in OS config directory like ~/.config/morphir or ~/.morphir)
  • Local overrides (per-project in .morphir/ directory)
  • Platform-specific artifacts using .NET Runtime Identifiers (e.g., linux-x64, win-x64, osx-arm64)

Dist Commands

Manage core Morphir distributions.

dist list

List installed distributions.

morphir dist list [options]

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: List local (project) installations only

Example:

# List all distributions for current platform
morphir dist list

# List distributions for a specific platform
morphir dist list --platform linux-x64

# List project-local distributions
morphir dist list --local

dist install

Install a distribution from a URL.

morphir dist install <url> <version> [options]

Arguments:

  • <url>: Source URL to download the distribution from
  • <version>: Version string (e.g., 1.0.0, 2.1.3-beta)

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Install locally (project-specific)

Example:

# Install globally
morphir dist install https://example.com/morphir-dist.tar.gz 1.0.0

# Install locally for project
morphir dist install https://example.com/morphir-dist.tar.gz 1.0.0 --local

# Install for specific platform
morphir dist install https://example.com/morphir-dist.tar.gz 1.0.0 --platform linux-x64

dist use

Set the active distribution version.

morphir dist use <version> [options]

Arguments:

  • <version>: Version to activate

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Set in local (project) scope

Example:

# Set global active version
morphir dist use 1.0.0

# Set local active version for current project
morphir dist use 2.0.0 --local

Resolution: Local selection takes precedence over global when both are set.

dist remove

Remove an installed distribution.

morphir dist remove <version> [options]

Arguments:

  • <version>: Version to remove

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Remove from local (project) scope

Example:

morphir dist remove 1.0.0
morphir dist remove 1.0.0 --local

dist which

Show the currently active distribution.

morphir dist which [options]

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)

Example:

morphir dist which
# Output: Version 1.0.0 (global) at /home/user/.config/morphir/dist/linux-x64/1.0.0

Tool Commands

Manage auxiliary CLI tools and utilities.

tool list

List installed tools.

morphir tool list [options]

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: List local (project) installations only

tool install

Install a tool from a URL.

morphir tool install <name> <url> <version> [options]

Arguments:

  • <name>: Tool name
  • <url>: Source URL
  • <version>: Version string

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Install locally (project-specific)

Example:

morphir tool install morphir-elm-codegen https://example.com/tool.tar.gz 1.0.0
morphir tool install my-formatter https://example.com/formatter.tar.gz 2.1.0 --local

tool use

Set the active tool version.

morphir tool use <name> <version> [options]

Arguments:

  • <name>: Tool name
  • <version>: Version to activate

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Set in local (project) scope

tool remove

Remove an installed tool.

morphir tool remove <name> <version> [options]

tool which

Show the currently active tool version.

morphir tool which <name> [options]

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)

Extension Commands

extension list and explicit local-developer installation are available. Installation uses a registered repository name, a release declaration and a local archive, with explicit unchecked/no-probe consent. A positional archive URL is not accepted. Selection and removal commands remain stubs. See extension installation status. The full installer is tracked by #558.

An extension adds a capability to Morphir, such as a code generator. The plan is in the Morphir extension model PRD.

morphir extension list [--json] [--platform <rid>] [--local]
morphir extension runtimes [--json]
morphir extension repository add <name> --directory <absolute-path> --home <absolute-home> --local-developer [--dry-run]
morphir extension install <name> --repository <registered-name> --local-release <json> --local-archive <zip> --home <absolute-home> --local-developer --no-probe [--dry-run]
morphir extension use <name> <version> [--platform <rid>] [--local]             # not implemented yet
morphir extension remove <name> <version> [--platform <rid>] [--local]          # not implemented yet
morphir extension which <name> [--platform <rid>]                               # not implemented yet

use, remove and which fail with exit code 1 and a link to issue #492. They arrive with step 3 of the PRD.

morphir extension runtimes reports whether this host can run WASM extensions, with its engine, profiles and bounds, or the reason it cannot. See morphir extension runtimes.

morphir extension worker is internal and experimental, and is not in this list or in the help: it serves one WASM module over stdio for a conformance runner, and no product command uses it. See morphir extension worker.

extension list

List the extensions that this host knows, and say which offering of morphir-dotnet is running and what it needs from the machine. Today the extensions are the built-in F# frontend of morphir compile, the built-in generators of morphir generate and the built-in analyzers of morphir validate.

$ morphir extension list
╭────────────────────────┬────────────────┬─────────┬─────────────────────┬──────────┬─────────────────────────╮
│ ID                     │ NAME           │ VERSION │ TYPES               │ DELIVERY │ HOSTS                   │
├────────────────────────┼────────────────┼─────────┼─────────────────────┼──────────┼─────────────────────────┤
│ morphir-dotnet.fsharp  │ F# frontend    │ 0.4.0   │ frontend (fsharp)   │ built-in │ morphir, dotnet-morphir │
│ morphir.csharp         │ C# generator   │ 0.4.0   │ backend             │ built-in │ morphir, dotnet-morphir │
│ morphir.fsharp         │ F# generator   │ 0.4.0   │ backend             │ built-in │ morphir, dotnet-morphir │
│ morphir.type-validator │ Type validator │ 0.4.0   │ validator (1, 2, 3) │ built-in │ morphir, dotnet-morphir │
╰────────────────────────┴────────────────┴─────────┴─────────────────────┴──────────┴─────────────────────────╯

The rows are sorted by id. The version of a built-in is the version of the host, including any build metadata. The TYPES column shows each type. For a frontend, the languages that it reads follow in parentheses; for a validator, the IR versions that it reads.

In a terminal the table has styled borders and headers and fits the terminal width. When stdout is redirected, or NO_COLOR is set, the table has no ANSI codes. The table is for people: scripts use --json.

Options:

  • --json: Write one JSON document on stdout.
  • --platform <rid>, --local: Select installed extensions. There are none until step 3 of the PRD, so these options do not change the result now. Built-in extensions always appear.

With --json, from the Native AOT morphir for Windows ARM64:

{
  "host": "morphir",
  "deployment": "native-aot",
  "execution": "native-aot",
  "platform": "win-arm64",
  "prerequisites": { "runtime": [], "sdk": [], "native": [] },
  "extensions": [
    {
      "id": "morphir-dotnet.fsharp",
      "name": "F# frontend",
      "version": "0.4.0",
      "description": "Compiles F# source files to a Morphir model in process.",
      "types": ["frontend"],
      "delivery": "built-in",
      "hosts": ["morphir", "dotnet-morphir"],
      "deployments": ["self-contained", "framework-dependent", "dotnet-tool"],
      "available": false,
      "reason": "The native-aot morphir has no in-process F# frontend; the self-contained and framework-dependent morphir and dotnet-morphir have one. Here, install the F# frontend process delivery morphir-fsharp-frontend and configure it with [frontend.fsharp].extension or --extension.",
      "prerequisites": { "runtime": [], "sdk": [], "native": [] },
      "capabilities": {
        "frontend": {
          "languages": [{ "id": "fsharp", "fileExtensions": [".fs", ".fsi"] }],
          "irVersions": ["1", "2", "3", "4"]
        }
      }
    },
    {
      "id": "morphir.csharp",
      "name": "C# generator",
      "version": "0.4.0",
      "description": "Generates C# source from a Morphir library model.",
      "types": ["backend"],
      "delivery": "built-in",
      "hosts": ["morphir", "dotnet-morphir"],
      "deployments": ["native-aot", "self-contained", "framework-dependent", "dotnet-tool"],
      "available": true,
      "prerequisites": { "runtime": [], "sdk": [], "native": [] },
      "capabilities": {
        "backend": { "targets": ["csharp"], "irVersions": ["1", "2", "3", "3.0.0", "3.1.0", "4", "4.0.0"], "generate": true }
      }
    }
  ]
}

The example leaves out the morphir.fsharp and morphir.type-validator entries. The four offerings report:

OfferinghostdeploymentexecutionplatformTop-level prerequisitesmorphir-dotnet.fsharp
Native AOT morphirmorphirnative-aotnative-aotthe RIDnonenot available, with the reason above
Self-contained morphirmorphirself-containedjitthe RIDnoneavailable
Framework-dependent morphirmorphirframework-dependentjitthe RIDruntime Microsoft.NETCore.App >=10.0.0 <11.0.0available
dotnet-morphir (Morphir.Tool)dotnet-morphirdotnet-tooljitleft out: the package is portablethat runtime, and SDK Microsoft.NET.Sdk >=10.0.100 with "phase": "install"available

A build that no named release profile produced, such as dotnet run, a test host, or the legacy morphir-<rid> single-file assets that releases attach today, has no qualified deployment: it leaves out deployment, platform and the top-level prerequisites, and never reports one of the four names. It still reports execution.

FieldMeaning
hostThe command that wrote the list: morphir or dotnet-morphir (the dotnet tool). It is a command name, not the offering: the Native AOT, self-contained and framework-dependent offerings all report morphir.
deploymentThe offering that the build’s named release profile set at build time: native-aot, self-contained, framework-dependent or dotnet-tool. It is never inferred from how the process runs. Left out when no named profile built the executable.
executionHow this process runs: native-aot or jit.
platformThe .NET runtime identifier that the profile built for. Left out for the portable tool package and when no named profile built the executable.
prerequisitesWhat the offering needs from the machine: runtime (an installed .NET runtime, to run), sdk (a .NET SDK, with phase install or run) and native (none is declared yet). Left out when no named profile built the executable.
deploymentsThe offerings in which the extension is available.
reasonWhy the extension is not available in the running offering. Present exactly when available is false.
extension prerequisitesWhat the extension needs from the machine beyond the host. Every built-in needs nothing.
idThe extension id. See Extension ids. For a built-in generator, it is also the backend value in morphir generate --json reports.
versionCanonical SemVer 2.0.0.
descriptionLeft out when the extension has none.
typesMEP extension types: frontend, backend, transform, validator or workspace. They come from the capabilities.
deliveryHow the extension is delivered: built-in (compiled into the host) or wasm (a WASM module that the host’s WASM runtime runs). Every entry listed today is built-in: listing configured WASM modules is planned in issue #565.
hostsThe hosts that the extension runs in.
availabletrue when the extension runs in this host and offering. An extension for the other host, or for other offerings, is listed with false and a reason. A wasm extension in a host whose WASM runtime is unavailable is listed with false.
capabilities.frontendThe MEP frontend claim: the languages with their file extensions, and the IR versions that it produces.
capabilities.backendThe MEP backend claim: targets, IR versions in their declared spelling, and generate.
capabilities.validatorThe IR versions that a validator reads, in the spelling of the model readers. This is a morphir-dotnet report member: MEP 0.2.0-draft.1 has no validator capability, so hosts do not write it into MEP messages.

Logs and diagnostics go to stderr, so morphir extension list --json | jq works. If two extensions have the same id, the command exits with code 2 and names each duplicate id (extension.duplicate_id). With --json, the document then has an empty extensions array and a diagnostics array.

Extension ids

An extension id is one or more segments joined by dots, at most 238 characters. Each segment starts with a lowercase ASCII letter, contains only lowercase letters, digits and -, and ends with a letter or digit. Examples: morphir.csharp, acme.tools.lint-2, morphir-elm.

A one-segment id follows the upstream Morphir rule. Dotted ids are a morphir-dotnet superset, proposed upstream in finos/morphir#1137.

Every id that starts with morphir. is reserved for the built-in frontends and backends of morphir-dotnet, including built-ins that do not ship yet. compile and generate refuse an extension guest that reports such an id (extension.unavailable). Ids such as org.finos.morphir.fsharp-backend are not reserved.

Platform Identifiers (RIDs)

Morphir uses .NET Runtime Identifiers to manage platform-specific artifacts:

PlatformRID
Linux x64linux-x64
Linux ARM64linux-arm64
Windows x64win-x64
Windows ARM64win-arm64
macOS x64 (Intel)osx-x64
macOS ARM64 (Apple Silicon)osx-arm64

The CLI automatically detects the current platform when --platform is not specified.

Resolution Rules

When looking for an active artifact, the CLI uses the following precedence:

  1. Project-local selection (if .morphir/ exists and selection is set locally)
  2. Global selection (in OS config directory)
  3. Error if no selection is found

Each artifact category (dist, tool, extension) resolves independently.

Folder Layout

Global (preferred):
  ~/.config/morphir/           (Linux)
  ~/Library/Application Support/morphir/  (macOS)
  %APPDATA%\Morphir\           (Windows)
    dist/
      <platform>/
        <version>/
          bin/
          manifest.json
    tools/
      <platform>/
        <tool-name>/
          <version>/
            bin/
            manifest.json
    extensions/
      <platform>/
        <extension-name>/
          <version>/
            bin/
            manifest.json

Local (optional, per project):
  .morphir/
    dist/...
    tools/...
    extensions/...

Manifest Format

Each installed artifact includes a manifest.json file (JSONC format with comments supported):

{
  "name": "morphir-dist",
  "version": "1.0.0",
  "platform": "linux-x64",
  "sourceUrl": "https://example.com/artifact.tar.gz",
  "sha256": "abc123...",
  "description": "Core Morphir distribution",
  "installedAt": "2025-12-23T19:00:00Z",
  "metadata": {
    "key": "value"
  }
}

See Also

7.3 - Config commands

Inspect and edit Morphir configuration through shared services

Config commands

morphir config get|list|show|path|explain reads configuration through the same shared APIs available to embedded hosts. It never edits project files, executes scripts, or resolves secret references. Existing IR and artifact-management commands keep their own configuration behavior; these inspection commands do not make them consume every setting.

Synopsis

morphir config get <key> [options]
morphir config list [options]
morphir config show [options]
morphir config path [options]
morphir config explain <key> [options]
morphir config set <key> <value> [--type <type>] [--dry-run] [options]
morphir config unset <key> [--dry-run] [options]
CommandResult
get <key>One typed value; a missing key is distinct from an empty string or array
listOrdered canonical key/value entries; arrays are complete values
showThe nested document
pathConsulted sources with status, role, format and ordering metadata
explain <key>Contributing layers, the effective value and winning origins

Keys are case-sensitive. Dots separate segments; JSON-escaped double-quoted segments select literal dots or special characters. Protect the complete path with shell single quotes:

morphir config get ir.format_version
morphir config get 'codegen."custom.generator".enabled' --json
morphir config list --show-origin --show-scope
morphir config show --json
morphir config path --json
morphir config explain custom.theme

Arrays are queried as a whole, without indexing syntax. custom.items.0 does not select an array element. Empty unquoted segments, trailing dots and unterminated quoted segments are usage failures.

Scope selectors

Effective configuration is the default. A scope selects its own sources before merging and never fills a missing key from defaults, environment values or another scope.

SelectorSources
--workspaceWorkspace primary files
--project <name-or-path>Primary files of one selected project; a relative path starts at the workspace root
--userPersonal overrides adjacent to the selected project or root
--globalGlobal user files
--systemSystem files
--file <path>One explicitly selected supported-format file

Selectors are mutually exclusive. --file and --config cannot be combined. --file reads a required file, even when it was not a discovered primary. --config overlays a required selected file during effective resolution. Relative file paths start at the invocation directory. Neither option writes anything.

An invalid workspace.default_member produces a selection error for effective, project and adjacent-user queries. Workspace, global, system and file scopes inspect their own sources independently of automatic project selection. File scope also excludes synthetic classic compatibility defaults, so an unauthored pipeline.backend remains missing there.

A workspace-root invocation reads the root unless shared selection chooses one default project. Invocation inside a member reads that member with applicable inherited settings. Use --workspace or an explicit project selector when inspecting a particular primary scope. Ambiguous or unknown project selectors fail visibly.

Resolution and output options

OptionPurpose
--show-originInclude winning paths, formats and available positions with entries
--show-scopeInclude winning source roles with entries
--jsonEmit one JSON report with typed JSON values and structured diagnostics
--config <path>Overlay an explicitly selected configuration file
--discovery-boundary <directory>Include this directory in discovery and exclude its parents. Default: MORPHIR_DISCOVERY_BOUNDARY, else the current filesystem volume
--profiles <names>Comma-separated profile names, repeatable in order
--no-profilesDisable profiles; conflicts with --profiles
--isolated [true|false]Exclude machine and personal files; retain active profiles and supplied environment values. Default: MORPHIR_ISOLATED, else false. --isolated false turns off isolation set in the environment
--suppress-warning <code>Suppress a supported warning, repeatable
--helpShow help without configuration discovery or loading

Profile names match [a-z][a-z0-9_-]{0,63}. Invalid explicit or environment profile names fail before discovery. --profiles ci --profiles metrics activates ci then metrics; use --no-profiles to disable activation. The supported mixed-format warning codes are config.yaml_beside_toml and config.json_keys_below_toml. Unknown suppression codes produce a structured warning.

Without an explicit boundary, the native CLI searches within the current filesystem volume. It captures native machine locations and environment values at execution time. Machine paths use their own filesystem roots, including other Windows drives and redirected UNC shares, independently of the project discovery boundary. Embedded hosts can instead supply bounded or mounted trees without ambient machine access. See the configuration guide.

Containment from the environment

A parent process, CI job or test harness can contain every config command without changing its arguments:

VariableMirrorsAccepted values
MORPHIR_DISCOVERY_BOUNDARY--discovery-boundaryA directory. A relative path resolves against the current directory. Native Windows drive and UNC paths work with either separator
MORPHIR_ISOLATED--isolatedtrue, false, 1 or 0, in any case

A flag on the command line wins over its variable, and the variable wins over the default. An empty variable counts as unset. Neither variable ever becomes a configuration value.

The command fails with exit code 2, before reading any configuration, when:

  • MORPHIR_ISOLATED has any other value. The containment.invalid_value diagnostic names the variable and the accepted values.
  • The current directory is outside the effective boundary. The discovery.boundary diagnostic names the boundary and where it came from, so an inherited variable is easy to spot.

--help never reads either variable.

$ export MORPHIR_DISCOVERY_BOUNDARY=$PWD MORPHIR_ISOLATED=1
$ morphir config path --json

config path --json reports what was in effect:

"containment": {
  "boundary": "/home/ada/repo",
  "boundarySource": "environment",
  "isolated": true,
  "isolationSource": "environment"
}

Each source is flag, environment or default.

JSON contract

Every read command emits one object with these fields. The JSON Schema is checked against subprocess results in the CLI behavior suite. No F# union or reflection-based serialization is exposed.

FieldType and meaning
operationget, list, show, path or explain
scopeeffective, workspace, project, user, global, system or file:<canonical-path>
keyCanonical quoted key path, or null
foundBoolean; distinguishes missing keys from present empty values
valueJSON scalar, array or object; null for a missing key or an operation without a value
entriesArray of {key, value, origins}
sourcesArray of {source, status, contributed, detail}
contributionsArray of {source, value, winner, origins}
diagnosticsArray of {code, severity, message, path, key, line, column}
valueTypesArray of {pointer, kind, encoding} for canonical types requiring JSON metadata
exitCodeInteger 0, 1 or 2
containmentpath only: {boundary, boundarySource, isolated, isolationSource}; omitted by other operations

A source is {path, format, role, order, profile}. format is json, yaml, toml or memory; role uses the canonical source-role name such as RootPrimary, MemberPrimary, Global or Environment. profile is a name or null. Source status is loaded, missing, skipped or failed; optional missing candidates remain in the source inventory.

An origin is {key, source, line, column}. Origins are empty unless requested for get/list/show entries; explanations always include them. With only --show-scope, path/format are empty and positions are null; with only --show-origin, the role is empty. order and profile metadata remain available. Composite values can have multiple winning origins.

For example, querying a present empty array returns a successful report containing:

{
  "operation": "get",
  "scope": "effective",
  "key": "custom.targets",
  "found": true,
  "value": [],
  "entries": [{ "key": "custom.targets", "value": [], "origins": [] }],
  "sources": [],
  "contributions": [],
  "diagnostics": [],
  "valueTypes": [],
  "exitCode": 0
}

Reports preserve the readers’ supported 128 levels of configuration nesting. Consumers using System.Text.Json should set MaxDepth = 256 when parsing deeply nested reports, allowing room for the report envelope and typed value tags.

The sources array above is abbreviated to emphasize the value contract. path and other result source inventories retain their actual consulted-source reports. Finite numbers and integers stay JSON numbers, including integers outside Int64. Nonfinite numbers use a tagged value such as {"$number":"NaN"}, {"$number":"Infinity"} or {"$number":"-Infinity"}. The envelope’s separate valueTypes entry identifies the canonical node with kind: "number", encoding: "nonfinite" and a JSON Pointer such as /value/custom/nan. An ordinary user object containing $number has no numeric metadata at that pointer and remains an object. This avoids reserving user keys.

For example these two value fragments have different canonical types:

{
  "value": {"$number": "NaN"},
  "valueTypes": [{"pointer": "/value", "kind": "number", "encoding": "nonfinite"}]
}
{
  "value": {"$number": "NaN"},
  "valueTypes": []
}

The first represents a nonfinite number. The second is an ordinary object. The complete report also carries the other required envelope fields listed above. Positive and negative infinity use the tags Infinity and -Infinity with the same external discriminator. Consumers use the metadata pointer, never user keys alone, to recognize special values.

Temporal values retain their string text and have kind: "temporal" metadata with the original temporal kind in encoding, for example LocalDate. Metadata also identifies special values under /entries/0/value and /contributions/0/value. Pointers use standard JSON Pointer escaping, including ~0 for ~ and ~1 for /. Finite values need no metadata. Redaction replaces sensitive values and secret references with "[REDACTED]" before presentation and before special-value encoding; safe key/source metadata remains inspectable. Diagnostics retain structured codes and metadata while avoiding raw secret-bearing input text.

Stdout contains only results. Human diagnostic text goes to stderr in both human and JSON modes. Successful JSON with mixed-format warnings therefore remains parseable, and diagnostics carries the warnings for scripts. Usage errors also return one structured JSON report when --json is present.

Exit codes

CodeMeaning
0Successful inspection, preview or edit, including unchanged and overridden writes
1Requested get/explain key or targeted unset entry not found
2Usage, validation, I/O, ambiguous selection, stale-plan conflict or recovery failure

IR verification retains its existing interpretation of code 1.

Malformed or inaccessible host appsettings files return config.load with exit code 2. With --json, the failure remains one JSON result. Invalid command arguments return config.usage before this failure; help does not load host configuration.

CI example

morphir config show --isolated --discovery-boundary . --profiles ci --json > effective-config.json
morphir config get ir.format_version --isolated --discovery-boundary . --profiles ci --json

Isolation preserves supplied environment values and selected profiles. Control the job’s environment as well when checking reproducibility. An explicitly selected eligible personal file retains its discovered ownership restrictions even under isolation.

Editing one TOML target

set and unset select one file, then use the same reusable planner and writer available to embedded hosts. Without a selector, an invocation inside a project targets its primary. At a declared workspace root it targets the workspace primary, including a virtual workspace. A missing or ambiguous default context fails before writing.

Use one of --workspace, --project <name-or-path>, --user, --global, --system or --file <path>. --user uses the selected project’s adjacent personal file, including a selected default member; a virtual root with no selected project uses its root-adjacent personal file. Existing personal files are considered across every eligible layout of that owner. Multiple personal TOML candidates remain ambiguous. An explicit personal file in another declared member retains that member’s ownership and effective context, including under isolation. Multiple selectors fail. Explicit --file targets must end in .toml. --config remains a selected read overlay and cannot be combined with --file.

The editor uses an existing TOML primary when present. A JSON/YAML-only scope receives the corresponding TOML primary at the same layout. It leaves lower-format files unchanged and refuses a new same-format primary ambiguity. Comments, unrelated entries, key spelling, ordering, line endings, BOM and accepted BOM encodings are preserved. Repeating an identical set does not rewrite the file or update its timestamp.

Strings are the default, even with --json. Explicit types use JSON literal syntax:

--typeInput
stringLiteral command argument, including an empty string
bool or booleantrue or false
int or integerSigned 64-bit JSON integer
numberFinite JSON number, retained as a TOML float
arrayComplete JSON array
objectComplete JSON object

Duplicate object keys, null values and nested nulls fail. Integer input and integer tokens inside composite values must fit Int64; explicit number input chooses finite floating-point precision, including for large literals. Arrays and objects replace the complete target entry. Paths address whole arrays and opaque references; traversal through an atomic ancestor fails. Known settings are validated in the authored target as well as the effective candidate, so a higher environment value cannot hide an invalid edit.

morphir config set ui.color false --type bool --global --dry-run --json
morphir config set custom.caption false --json  # writes the string "false"
morphir config set custom.targets '["csharp"]' --type array
morphir config set custom.generator '{"enabled":true,"targets":["csharp"]}' --type object
morphir config set 'codegen."custom.generator".enabled' false --type bool
morphir config unset custom.caption --user --dry-run

unset removes only the target’s authored entry. It adds no tombstone and never deletes an inherited value from another file. Removing an override can reveal JSON, YAML, another scope or a default. A missing target entry exits 1. A write that remains overridden still exits 0; the report identifies its effective value and winning origins.

Profile, isolation, warning-suppression and discovery-boundary controls retain their read meanings. The explicit edit target is read for editing even when isolation excludes it from the resulting effective view. Independent workspace/global/system/file selectors do not turn an invalid automatic member selection into a target selection. Effective explanations use a valid selected member when available, so a successful workspace write can remain overridden by that member.

Editing JSON reports

Every editing invocation with --json emits one object matching the editing JSON Schema. Stdout contains only this report; safe diagnostics go to stderr.

FieldMeaning
operationset or unset
outcomepreview, written, unchanged, conflict, rolled-back, recovery-required, io-failure or failed
target, keySelected source metadata and canonical quoted path, or null before target selection
before, afterRedacted target-entry values; null represents an absent entry
changed, dryRunWhether the proposed target differs, and whether this is a preview
effectiveThe nested inspection explanation with contributions, origins and its own type metadata
diagnosticsSafe codes and source/key/position metadata
recoveryArtifactsRetained {path, kind, transactionId} records
exitCodeStable outer command category, 0/1/2
valueTypesType metadata for outer before/after values

A nested effective explanation can report a missing value while a successful unset still returns outer exit 0. changed describes the proposed edit; after a conflict or rollback it does not claim that the candidate remains installed. Sensitive names and opaque reference shapes are redacted before rendering, including raw before/after input. No command executes a reference or resolves a secret.

Preconditions and recovery

--dry-run performs no writer acquisition, staging or filesystem change. Application rechecks the plan’s observed source/target identities, discovery inventories and absent alternate candidates under writer coordination. A stale source, destination or alternate-primary observation produces a conflict and leaves the preexisting destination unchanged. Reload, inspect the competing change and create a fresh plan.

Native writes stage on the destination filesystem and atomically install each file. The reusable transaction service supports multiple destinations for future callers, but does not promise group or crash atomicity across files. Generated-output publication and pruning manifests are separate and unchanged.

Native configuration writers use machine-local OS coordination. An embedded host writing a shared store from several machines must supply distributed coordination itself. External editors that do not participate can race with the final filesystem operation; this is an optimistic precondition protocol, not a lock on arbitrary applications.

After a complete rollback, the command exits 2 and cleans its temporary artifacts. An incomplete rollback or cleanup returns typed recovery information with retained paths and ownership. Preserve those files, inspect the target and journal, and restore or reconcile from the matching backup before retrying. Do not remove artifacts from another transaction. See troubleshooting.

Native filesystem access keeps the existing no-follow policy. On macOS, the conventional /etc path is a symlink, so the default system target can return an I/O-category failure before writing. Use an explicitly authorized physical TOML path when needed. A permission failure likewise returns 2; previews do not request elevated access or create directories.

Project migration

Use project migrate to adopt one classic project, or workspace migrate for a bounded repository. Both use loss-checked shared planning and guarded application. Editing does not remove classic manifests, perform migration equivalence checks or adopt workspace membership automatically.

7.4 - Project migration

Migration of one classic morphir-elm project to the newer Morphir project system

Project migration

morphir project migrate previews and applies one loss-checked conversion. The command calls shared migration services that other hosts can use without invoking the CLI. See the complete command reference for before/after examples, JSON schema and recovery.

Use it to adopt one classic morphir.json project into the newer project system. It does not convert source code, migrate morphir-ir.json, or choose a new IR version. For a repository-wide plan, use workspace migration.

Synopsis

morphir project migrate [<path>] [--output <toml-path>] [--dry-run] [--json]

<path> identifies one project directory or its classic morphir.json. When omitted, select the current unambiguous project. A member invocation does not implicitly migrate its siblings or change workspace membership.

morphir project migrate ./packages/orders --dry-run
morphir project migrate ./packages/orders
morphir project migrate packages/orders/morphir.json --dry-run --json
morphir project migrate morphir.json --output .config/morphir/config.toml

Options

OptionPurpose
--output <path>TOML target in a supported primary layout at the same project location; defaults to adjacent morphir.toml
--dry-runReturn mappings, edits, conflicts, and verification without writing
--jsonEmit a structured migration plan/result and diagnostics

Output selection must preserve the project location and relative-path meaning. An output that introduces same-format primary ambiguity is refused.

Migration behavior

Map name, sourceDirectory, and exposedModules to their canonical project fields. Preserve supported additional settings and materialize legacy-derived defaults, including Elm frontend and checkpoint behavior, so the new TOML works without the classic source.

If a TOML target exists, add only nonconflicting missing values while preserving comments, formatting, and unrelated settings. Identical existing values are unchanged. Different values or same-file legacy/canonical conflicts block the plan. Every source setting must be mapped, preserved with equivalent meaning, or reported as blocking. Unsupported dependency or decoration shapes cannot be silently dropped.

Before application, compare project-scoped resolution before migration with the candidate TOML while excluding the classic source from the candidate. Exclude ambient machine, personal, environment, CLI, and activated-profile overlays from both sides so they cannot hide a mismatch. Retain the declared base workspace context and compare resolved path meaning as well as values. Report the expected JSON-to-TOML provenance changes separately.

Only a verified plan can be applied. Recheck source, target, and relevant workspace preconditions before atomic target replacement. Keep morphir.json byte-identical for older consumers; removal is a separate deliberate action after those consumers have moved. Repeating a completed migration is a no-op.

Results and exit codes

The result identifies source/target paths, mappings, planned or applied edits, blockers, equivalence status, and expected origin changes. Sensitive values are redacted. Stdout contains results only; diagnostics go to stderr. JSON mode includes structured diagnostics in the result.

CodeMeaning
0Verified success, applicable preview, or unchanged repeat
2Invalid input, unsupported field, conflict, failed equivalence check, I/O failure, or recovery required

Validation and stale-plan failures leave source and target unchanged. If a write failure cannot be fully recovered, report the affected paths and retained recovery data rather than claiming success or discarding evidence.

7.5 - project migrate

Preview and apply a loss-checked migration from classic morphir.json to TOML

project migrate

morphir project migrate [path] [--output path] [--dry-run] [--json] [--discovery-boundary directory]

Migrate one classic project configuration to TOML. The command maps classic fields, checks that every original value is accounted for, and verifies the candidate independently of the classic manifest. It keeps morphir.json byte-identical for older consumers.

path accepts one project directory or classic morphir.json. With no path, shared discovery selects the current unambiguous project. Invoking from a workspace member selects that member. An unlisted project directory is refused; migration never substitutes a sibling or a containing root project for it. A virtual workspace with no unambiguous project requires a member directory or manifest path.

Options

OptionMeaning
--output pathChoose a supported TOML primary layout at the same project location
--dry-runReturn the complete plan without creating or changing files
--jsonEmit one structured JSON result on stdout
--discovery-boundary directoryBound discovery to this directory tree. Default: MORPHIR_DISCOVERY_BOUNDARY, else the filesystem root. The current directory and path must lie inside it; otherwise the command fails with exit code 2 and a discovery.boundary diagnostic before reading anything

Migration always excludes machine and personal configuration, so MORPHIR_ISOLATED does not apply. See containment from the environment.

The default output is the corresponding TOML primary adjacent to the selected classic manifest: morphir.json becomes morphir.toml; a dot-config config.json becomes config.toml. Supported primary layouts are morphir.toml, .morphir/morphir.toml, and .config/morphir/config.toml within the same project. Hidden and dot-config layouts retain the project’s location as the anchor for relative source paths. An arbitrary TOML filename, another project directory, a lower-format output, or an output that creates primary ambiguity blocks migration.

Before and after

A classic manifest:

{
  "name": "Orders",
  "sourceDirectory": "elm-src",
  "exposedModules": ["Order"]
}

The equivalent TOML includes the implicit legacy defaults:

[project]
name = "Orders"
source_directory = "elm-src"
exposed_modules = ["Order"]

[frontend]
language = "elm"

[pipeline]
backend = "checkpoint"
checkpoint_format = "morphir-json"

Migration does not add an unrelated ir.format_version value. Supported extra settings survive conversion. A raw field ledger records mapped, retained, derived and blocking values. Unsupported nonempty dependency/decorations shapes, nulls, values TOML cannot represent, and same-file classic/canonical conflicts block application. Resolve the reported fields deliberately before retrying. There is no drop-unsupported or overwrite switch.

Existing equal TOML entries keep their original spelling and comments. Missing values are added with the source-preserving editor. Different values block the entire write. A successful repeated migration returns unchanged and preserves the target timestamp.

Verification

The comparison resolves project-scoped typed settings and resolved path meaning before and after migration. It excludes the converted classic source from the candidate and excludes machine, personal, environment, explicit CLI and activated profile overlays from both sides. Declared base workspace settings remain. Expected origin changes are reported separately from value differences.

This means an ambient override cannot hide a bad conversion, and the retained classic file cannot supply a value omitted from TOML. Profile declarations and other supported source settings remain in the field ledger and conversion.

Examples

# Review the current project without writes.
morphir project migrate --dry-run

# Preview one member in JSON for CI review.
morphir project migrate packages/orders/morphir.json --dry-run --json > migration.json

# Use a dot-config primary at the same project location.
morphir project migrate packages/orders \
  --output packages/orders/.config/morphir/config.toml

# Apply the default adjacent target, then verify the repeat is unchanged.
morphir project migrate packages/orders
morphir project migrate packages/orders --json

Relative output arguments resolve from the invocation directory. This command adopts configuration only. It performs no source-code conversion, IR conversion, IR-version upgrade, sibling batch operation or workspace-membership edit.

Results and exit codes

ExitMeaning
0Verified application, applicable preview, or unchanged repeat
2Invalid input, loss, conflict, failed verification, stale evidence or write/recovery failure

Human output lists the outcome, source/target, field mappings, proposed edits and verification. JSON follows the project migration result schema. The report includes redacted values, loss ledger, planned edits, independent verification, origin changes, structured diagnostics and retained recovery paths. Diagnostics go to stderr; JSON stdout contains one valid document, including argument and host-initialization failures. Secret references are never executed.

Troubleshooting and recovery

  • For an IR-file rejection, pass the project directory or morphir.json.
  • For an unmapped field or unrepresentable value, inspect its raw path in the ledger and make a deliberate supported equivalent before retrying.
  • For an existing-target conflict, reconcile the reported values manually. Migration will not overwrite a differing TOML value.
  • For ambiguity, retain one primary per format group across supported layouts.
  • For a stale plan, preview again. Source, target and relevant workspace evidence are checked again at application time.
  • For rolled-back, the transaction restored the original target. Inspect the diagnostic, correct the write failure and plan again.
  • For recovery-required, preserve the reported journal, stage and backup paths. Inspect their transaction ownership before any manual recovery. Do not remove the original classic file or overwrite later user edits.

Writes reuse the configuration editor’s staged per-file atomic replacement and explicit recovery reporting. They do not claim multi-file crash atomicity. Rollback does not automatically delete a subsequently edited TOML. Removal of a classic manifest is a separate user action after older consumers have moved.

See configuration editing for transaction and host boundaries, and the configuration guide for discovery and layering.

7.6 - Workspace migration

Bounded adoption of classic projects into a Morphir workspace

Workspace migration

morphir workspace migrate coordinates the shared project migrator, bounded inventory, aggregate verification, and recoverable application. Other hosts use the same library services. The complete command reference describes the JSON contract and recovery outcomes.

Use it when a repository contains several classic projects, including when the repository does not yet declare a Morphir workspace. For one project without workspace membership changes, use morphir project migrate.

Synopsis

morphir workspace migrate [<directory>] [--include <pattern>] [--exclude <pattern>] [--dry-run] [--json]

The supplied directory, or the current directory when omitted, bounds the inventory and resulting edits. The command does not walk ancestors to find more projects to adopt.

morphir workspace migrate ./repo --dry-run
morphir workspace migrate ./repo --include 'packages/*' --exclude 'packages/experimental-*' --dry-run --json
morphir workspace migrate ./repo

Options

OptionPurpose
<directory>Root of the bounded inventory; defaults to the current directory
--include <pattern>Refine candidates using a root-relative pattern; repeatable
--exclude <pattern>Exclude candidate paths using a root-relative pattern; repeatable
--dry-runPreview the complete workspace and project plan without writing
--jsonEmit a structured plan/result and diagnostics

Inventory stays within the selected root, skips discovered links, stops at nested declared workspaces, and excludes version-control, dependency, and build-output directories. Existing workspace exclusions remain effective.

Membership and project plans

The preview lists candidates, exclusions, nested boundaries, already migrated members, project edits, workspace edits, blockers, and verification results in deterministic path order.

Propose explicit root-relative member paths while preserving existing membership, settings, and a valid default member. Already migrated members remain members without being rewritten. A root project retains its identity; a root without a project is a virtual workspace. Duplicate project names are diagnosed without merging their distinct paths.

Reuse the single-project migration contract for every selected classic project. One blocked project blocks the batch. Verify each classic-to-TOML conversion in the same proposed workspace context, excluding the converted classic source from the candidate and ambient overlays from both sides. Preserve source-path meaning and project settings independently of serialization.

The plan separately reports intended workspace adoption effects: the new topology, member/default selection, and shared output root. For example, standalone defaults under each member’s .morphir/out can become <workspace>/.morphir/out. This declared adoption effect is not a conversion mismatch. Owning workspace context supplies topology and the output root; members do not inherit the root’s entire project or workspace document subtrees. No existing generated files are moved by migration.

Check the resulting workspace as a whole against those declared effects. Do not relax loss checks or rely on classic manifests or ambient overrides to hide an unexplained setting change.

Application and recovery

Validate and stage the entire plan before replacing any destination. Recheck relevant source and target preconditions, coordinate overlapping writers, and journal replacements so a commit failure can attempt rollback.

Multiple file replacements are not crash-atomic. Report restored and unresolved paths explicitly if rollback fails. Retain recovery data and block overlapping writes until recovery is resolved. A preview encountering pending recovery reports recovery-required with exit 2 and the retained metadata without modifying it. Successful application and complete rollback clean their temporary artifacts. Every classic morphir.json remains unchanged.

An equivalent repeat reports unchanged files without rewriting them. A valid existing workspace needing no further conversion is a successful no-op. An undeclared root with no eligible projects reports that no adoption can be planned.

Results and exit codes

Stdout contains results only; diagnostics go to stderr. JSON results include structured diagnostics and per-project outcomes, with sensitive values redacted.

CodeMeaning
0Verified success, applicable preview, or unchanged existing workspace
2Invalid or blocked plan, failed verification/application, or recovery required

7.7 - workspace migrate command

Preview and apply bounded workspace adoption with per-project loss checks and recoverable writes

workspace migrate

morphir workspace migrate adopts eligible projects within one selected directory. It uses the reusable inventory, aggregate planner/verifier, and transaction APIs. The same operation is available to embedded hosts without invoking this command.

Synopsis

morphir workspace migrate [<directory>]
    [--include <pattern>]... [--exclude <pattern>]...
    [--dry-run] [--json] [--discovery-boundary <directory>]

<directory> defaults to the current directory. It must name an existing directory. That directory bounds inventory, discovery, and all proposed destinations, including when it contains no workspace declaration. Migration does not search ancestors for projects to adopt. Select the intended repository root explicitly when invoking from a member or source directory.

Workspace migration never reads machine or personal configuration, so MORPHIR_ISOLATED and --isolated do not apply to it. See containment from the environment.

Arguments and options

ControlMeaning
<directory>Bounded root; omitted means current directory
--include <pattern>Include candidate project directories matching a root-relative pattern; repeatable
--exclude <pattern>Exclude candidate project directories; repeatable
--dry-runReturn the complete verified plan and write nothing
--jsonWrite one redacted JSON result to stdout; diagnostics remain on stderr
--discovery-boundary <directory>Outer limit the directory must lie within. Default: MORPHIR_DISCOVERY_BOUNDARY, else the filesystem root. A directory outside it fails with exit code 2 and a discovery.boundary diagnostic, and nothing is written
--helpShow usage without discovery or migration

Patterns are relative to the selected root. * and ? match within a path segment; ** matches across directory segments. Absolute patterns and .. segments are invalid. Exclusions prune matching subtrees. Shell-quote wildcards so the shell does not expand them before the CLI receives them. Prefix a literal leading-dash pattern with ./, for example --include './--experimental', so it cannot be mistaken for a missing value followed by an option. Existing workspace.exclude settings remain effective alongside the supplied exclusions. Explicit filters do not override nested workspace, link, version-control, dependency, or build-output boundaries. Invalid controls block before application.

morphir workspace migrate . --dry-run
morphir workspace migrate ./repo --dry-run --json
morphir workspace migrate . --include 'packages/*' --include 'services/*' \
  --exclude 'packages/experimental-*' --exclude 'services/retired' --dry-run --json

Inventory and membership

Inventory reports project candidates, excluded paths, skipped links, nested declared workspace boundaries, and already migrated projects in deterministic path order. It remains bounded even without an existing workspace. It skips generated output, version-control, and dependency directories and does not follow discovered links.

The plan uses explicit root-relative paths for new members. Existing membership, exclusions, settings, and valid workspace.default_member remain in place. Already migrated members are retained without rewriting their primaries. A root project keeps its identity. A root without a project becomes a virtual workspace. Duplicate project names produce diagnostics while their distinct paths remain separate.

For example, a new virtual workspace containing packages/orders and packages/payments receives:

[workspace]
members = ["packages/orders", "packages/payments"]

Each classic project receives its own supported TOML primary. Hidden primary layouts keep the owning project directory as the source-path anchor.

Conversion and verification

Every selected classic project uses the single-project migration contract, including the complete raw loss ledger, unsupported-field blockers, conflict checks, and materialized legacy defaults. A blocked member blocks the aggregate before any destination replacement.

Migration compares each classic source and its TOML conversion inside the same proposed base workspace context. It excludes converted classic sources from the candidate and ambient personal, machine, environment, explicit CLI, and activated profile overlays from both sides. Declared profile controls are still checked. The comparison preserves typed values and source-path meaning and checks the resulting workspace discovery, membership, and selection.

Adoption effects appear separately in the plan: ownership, membership, default selection, and shared output roots. Standalone projects may acquire a shared <workspace>/.morphir/out output context. That effect cannot excuse unrelated setting or source-path changes. Migration moves no generated artifacts and keeps every classic source byte-identical.

Output

Human output names the outcome, inventory entries, member paths, project edits, workspace edits, adoption effects, and verification. Diagnostics go to stderr.

JSON follows the workspace migration result schema. Its fields include:

FieldContent
operation, outcome, exitCodeOperation identity, result state, and exit code
rootPath, rootIsProjectBounded root and root versus virtual identity
changed, dryRun, verifiedPlanned changes, preview mode, and aggregate verification
inventoryPath, relative path, kind, reason, and project name
projectsPer-project redacted ledger, edits, conflicts, and independent verification
members, workspaceTarget, workspaceEditsReviewed membership and workspace primary edits
adoptionEffectsIntentional ownership, membership, default-selection, and output-root effects
diagnosticsStructured warnings and blockers also sent to stderr
recoveryArtifacts, restoredPaths, unresolvedPathsRetained owned evidence and recovery state
uncertainDirectories, preservedDirectoriesDirectory paths whose ownership is uncertain, or that explicit recovery was instructed to preserve
valueTypesTyped encoding metadata for nonfinite and temporal values in workspace edits

Each nested project report has its own valueTypes array. Its JSON pointers are relative to that project report. Sensitive settings and opaque secret references are redacted throughout the reports; reference targets are never resolved.

morphir workspace migrate . --dry-run --json > workspace-preview.json
morphir workspace migrate . --json > workspace-result.json
morphir workspace migrate . --json # unchanged after successful adoption

Exit codes

CodeMeaning
0Applicable preview, successful application, or valid existing-workspace no-op
2Invalid input, blocked conversion, failed verification, stale evidence, failed application, or recovery required

An undeclared directory with no eligible projects reports no adoption plan and exits 2. A valid existing workspace requiring no conversions succeeds unchanged. Equivalent repeats preserve target bytes and timestamps.

Application and recovery

Application validates and stages the entire aggregate before any destination is replaced. It coordinates overlapping cooperating writers, rechecks source, target, listing, and topology preconditions, and records durable recovery evidence. A later replacement failure triggers rollback attempts. The result identifies restored and unresolved paths.

Per-file atomic replacement does not make the batch crash-atomic. Incomplete rollback or interrupted state retains journals, stages, and backups and produces recovery-required. A read-only preview also reports pending recovery with exit 2 and retains the journal metadata. Overlapping configuration editing and project/workspace migration writes remain blocked until explicit recovery resolves that state. Successful application and proven complete rollback remove only owned transaction artifacts. User changes and unresolved evidence are preserved. If interruption occurs after directory creation but before its ownership receipt returns, uncertainDirectories names those paths explicitly. Recovery does not claim or delete them. The shared recovery API can explicitly preserve reviewed uncertain directories and reports them in preservedDirectories.

Use the shared inspection and recovery APIs. There is no automatic cleanup of uncertain recovery state. Do not delete a journal or backup to force a repeat migration through. Classic sources are never removed by migration or recovery.

Troubleshooting and automation

For a blocker, inspect projects[*].ledger, projects[*].conflicts, and diagnostics before changing source or target settings. A fresh preview is needed after source, target, relevant listing, or topology changes. A shared-output-root adoption effect does not permit a different source directory.

CI should inspect the exit code and retain a failed JSON result as an artifact:

morphir workspace migrate "$REPOSITORY" --dry-run --json > workspace-plan.json

A preview still requires independent review before choosing to apply it. No release, deployment, source deletion, or generated-output publication is part of this operation. Migration is owned by project and workspace; there is no config migrate alias.

See the walkthrough, workspace overview, and troubleshooting.

7.8 - mck

Run the separately installed Morphir Compatibility Kit, morphir-mck

mck

morphir mck <args> runs morphir-mck <args>, the Morphir Compatibility Kit (MCK), the way dotnet morphir runs dotnet-morphir. The kit is a development tool from finos/morphir, installed separately. This CLI does not include it and installs nothing: without morphir-mck, morphir mck only says where it looked.

The commands and options of the kit are those of morphir-mck. See its own help with morphir mck --help or morphir-mck --help.

Synopsis

morphir mck [<argument>...]
dotnet morphir mck [<argument>...]

Every entry point routes mck the same way: the Native AOT morphir, the managed morphir and dotnet-morphir. This pass-through uses MorphirApplication.Run. A custom host that parses through CreateApp first gets System.CommandLine’s help and response-file expansion before the routing action.

What reaches the tool

  • Arguments. Every argument after mck, in order and unchanged. The CLI does not read them, so --help, --version and @file go to the tool.
  • Working directory and environment. The CLI’s own.
  • Standard input, output and error. The CLI’s own handles, so pipes, redirection and terminal colours behave as for morphir-mck run directly. The CLI writes nothing to standard output when it routes.
  • Exit code. morphir mck exits with the exit code of morphir-mck.

How morphir-mck is found

  1. If MORPHIR_MCK is set and not empty, it must be the absolute path of the tool. A relative path is refused, so the result never depends on the current directory. On Windows it must be an .exe file. On Linux and macOS it must have an execute permission.
  2. Otherwise the folders of PATH are searched in order. Only absolute folders count: empty and relative entries, . included, are skipped. The first folder that holds morphir-mck.exe (Windows) or an executable morphir-mck (Linux and macOS) wins.

The current directory and the folder of morphir itself are never searched. On Windows, scripts such as morphir-mck.cmd are not candidates, because they would run through a shell.

When the tool is missing

$ morphir mck run
morphir mck runs the Morphir Compatibility Kit, morphir-mck, which is installed separately: it is a development
tool from finos/morphir, and this CLI does not include it. MORPHIR_MCK is not set, and no absolute folder on PATH
holds morphir-mck. ...
$ echo $?
127

The message goes to standard error, and standard output stays empty.

Interrupts and termination

  • Ctrl+C reaches morphir-mck directly. The tool decides how to stop, and morphir mck waits for it and exits with its code.
  • Windows. Before it starts the tool, morphir joins a job object that ends every process in it when morphir ends. If morphir is killed, for example from Task Manager or by a CI runner, the tool and every process it started stop too. This also stops a process that the tool meant to leave running after it exits. If the job cannot be joined, the CLI warns on standard error and continues without that kill-on-close protection.
  • Linux and macOS. SIGTERM or SIGHUP to morphir stops the tool’s process tree, and morphir exits with 143. SIGKILL to morphir cannot be passed on, so the tool keeps running in that case.

Exit codes

CodeMeaning
The tool’s codemorphir-mck ran.
126The resolved file could not be started.
127No tool to run: none found, or MORPHIR_MCK was refused.
143Linux and macOS: morphir received SIGTERM or SIGHUP and stopped the tool.

Only mck is routed

mck is the only command that runs another executable. Any other unknown command, such as morphir lint, is a command-line error, even when an executable morphir-lint is on PATH.

7.9 - morphir extension runtimes

Report whether this host can run WASM extensions, with its engine, profiles and bounds, or the reason it cannot

morphir extension runtimes

Reports the extension runtimes of the host you run. Today there is one, wasm: the runtime that runs WASM extension modules. The report says whether it is available in this executable, which engine runs the modules, which profiles the host runs and which bounds it enforces. When the runtime is unavailable, the report gives the reason.

Synopsis

morphir extension runtimes [--json]
dotnet-morphir extension runtimes [--json]

Options:

  • --json: Write one JSON document on stdout.

Description

The command loads no module. It asks the executable you run, not its file name or its install folder, so a copy of morphir that lost its engine library reports that copy’s state.

The host checks the engine only when something needs it: this command, or a command that runs a WASM module. It checks once per process. Starting morphir, --version, --help, extension list with only built-in extensions, and every other command that needs no WASM never load the engine. A missing or broken engine therefore cannot break them.

The command exits with 0 whether the runtime is available or not: the report is the result. Logs and diagnostics go to stderr, so morphir extension runtimes --json | jq works.

Output

An available runtime:

$ morphir extension runtimes
Host: morphir
Platform: win-arm64
wasm: available
  Engine: wasmtime 50.0.1
  Profiles: extism-kernel-v1
  Bounds: memory ceiling 268435456 bytes (256 MiB), call deadline enforced, instruction budget enforced
{
  "host": "morphir",
  "platform": "win-arm64",
  "runtimes": [
    {
      "kind": "wasm",
      "available": true,
      "engine": { "name": "wasmtime", "version": "50.0.1" },
      "profiles": ["extism-kernel-v1"],
      "bounds": { "memoryBytes": 268435456, "deadline": "enforced", "budget": "enforced" }
    }
  ]
}

An unavailable runtime, here a dotnet-morphir whose engine library is not in its folder:

$ dotnet-morphir extension runtimes
Host: dotnet-morphir
Platform: win-arm64
wasm: unavailable (engine_missing)
  Engine: wasmtime 50.0.1
  Reason: the engine's native library is not in this installation
{
  "host": "dotnet-morphir",
  "platform": "win-arm64",
  "runtimes": [
    {
      "kind": "wasm",
      "available": false,
      "reason": "engine_missing",
      "engine": { "name": "wasmtime", "version": "50.0.1" }
    }
  ]
}
FieldMeaning
hostThe command that wrote the report: morphir or dotnet-morphir. The Native AOT morphir and the managed morphir both report morphir.
platformThe platform the process runs on, named as the engine package names its native folders: win-x64, win-arm64, linux-x64, linux-arm64, osx-x64, osx-arm64, or another name such as linux-musl-x64 for a platform without an engine.
runtimes[].kindwasm.
runtimes[].availabletrue when this executable can run a WASM module.
runtimes[].reasonOnly when available is false: why. See Reasons.
runtimes[].engineThe engine this build carries, by name and version. It is the version of the Wasmtime package the build references, also when the engine cannot load.
runtimes[].profilesOnly when available: the WASM execution profiles the host runs. This host runs extism-kernel-v1 only.
runtimes[].boundsOnly when available: memoryBytes is the ceiling of each linear memory of a session, and deadline and budget say that the call deadline and the instruction budget are enforced. The values are the defaults of the pinned WASM execution profile contract.

The report has no member for the deployment offering (native-aot, self-contained, framework-dependent or dotnet-tool). The host identity now records named release profiles, but this report has not yet added those fields. It does not guess a deployment from the file name or path. A runner that needs the offering is given it as an input.

Reasons

ReasonWhat it meansWhat you can do
platform_unsupportedThe engine has no build for this platform, for example Linux with musl (Alpine).Use a supported platform for WASM extensions. Every other command works here.
engine_missingThe engine’s native library (wasmtime.dll, libwasmtime.so or libwasmtime.dylib) is not where the host looks for it. A copy, an archive or a package lost the file.Reinstall or extract the release again, keeping every file beside the executable, including the runtimes folder of the dotnet tool.
engine_load_failedThe library is there but did not load: it is for another architecture, it is damaged or of another version, or one of its own dependencies is missing.Reinstall the release for your platform and architecture. If that does not help, report it with the output of this command.
win_x64_trap_unsupportedWASM execution on Windows x64 is unavailable because trap handling is not compatible with the required process protections. The host refuses before loading the engine or reading a module.Use another supported platform for WASM. Other CLI commands remain available. The protection-preserving fix is tracked in #609.

No configuration or environment setting enables WASM on Windows x64. CET, context validation, signal-based traps and Spectre mitigations retain their defaults. No other command loads the engine.

The host decides the reason the first time something needs the engine: this report, loading a module, or listing a WASM extension. --version, --help and the worker’s argument and build-identity checks never load it.

Commands that need the runtime

A command that needs the WASM runtime in a host without it fails with extension.runtime_unavailable before it reads the module, and names the host, the platform and the reason. It never shows a native library loading error. Today the only such command is the internal morphir extension worker. It checks its arguments, then the build identity, then the engine, then the module, and reports the first that fails. Without a usable engine it writes one refused event with the code extension.runtime_unavailable on stderr, nothing on stdout, and exits with 3:

{"profile":"morphir/command-event","contractVersion":"0.1.0","command":"extension worker","sequence":1,"event":"refused","error":{"code":"extension.runtime_unavailable","message":"The WASM module of extension 'python' at '/work/python.wasm' cannot be loaded with extension.runtime_unavailable: the WASM runtime of dotnet-morphir 0.4.0 on linux-musl-x64 is unavailable (platform_unsupported): the engine has no build for this platform."}}

In extension list, a wasm extension has available set to false in a host whose runtime is unavailable, and built-in extensions stay available. See extension list.

See also

7.10 - morphir mck wasm-host (experimental)

Experimental and internal: run the crafted WASM host cases against a host’s worker mode, and check the report

morphir mck wasm-host

Development runner adapter reference. Product morphir mck ... forwards every argument to the separately installed morphir-mck, as described in morphir mck. The product contains no compatibility runner. The commands below document the retained adapter source awaiting migration to the dedicated development entry point. They do not establish that the installed tool supports these subcommands. Reports contain observations only; qualification awaits parent task 1.4. Contributors: see WASM host qualification.

Synopsis

morphir mck wasm-host run --kit <dir> --host <executable> --report <file>
    --offering <name> --platform <rid> --engine <text>
    [--host-arg <arg>]... [--host-profile <profile>]...
    [--deviations <file>]
    [--guest-kit-report <file> --guest-kit-host-sha256 <hex> --guest-kit-module-sha256 <hex>]

morphir mck wasm-host report-check --kit <dir> --report <file>
    [--expect-offering <name>] [--expect-platform <rid>]

Description

run reads the wasm-host suite (conformance/suites/wasm-host) and the pinned corpus it is generated from, and runs every case through the host’s worker mode:

<host> [--host-arg ...] extension worker --abi <abi> --module <wasm> --build-identity <id> [--request-bytes <n>] [--response-bytes <n>] [--instructions <n>] --json

The limits and the instruction budget are passed only for a case that sets them (worker.requestBytes, worker.responseBytes, worker.instructions); otherwise the worker keeps the contract’s defaults. It reads the build identity once from <host> extension worker --help --json. Before a case runs, it compares the module’s SHA-256 with the digest the corpus records, and refuses the case when they differ. It writes the case’s frames on stdin, reads the stdout frames and the stderr events, and compares them with the case’s expectation. A worker still alive one call deadline per frame plus the corpus margin (2 seconds) after its last frame has its process tree stopped. The runner knows nothing about the host except its path and arguments.

report-check reads a report back, checks it against the same kit and recomputes every outcome from the recorded observations. It never trusts a stored outcome.

Neither command computes a qualification verdict: the verdict belongs to parent task 1.4, which is not done. A report holds per-case observations and outcomes, and says so in its qualificationNotice.

Options of run

OptionMeaning
--kitThe suite directory, for example conformance/suites/wasm-host. The pinned corpus is found in the checkout that holds it.
--hostThe host executable.
--host-argAn argument before extension worker, repeatable. For example --host dotnet --host-arg exec --host-arg worker.dll.
--host-profileA profile the host declares it runs, repeatable. extism-kernel-v1 is always run; give morphir-bytes-v1 for a host that runs the byte profile. A case for a profile the host does not declare is not_applicable.
--offering, --platform, --engineRequired. What the host declares about itself, for example native-aot, linux-x64 and wasmtime 50.0.1. A worker reports none of them, so the runner is given them, and the report records them (corpus convention identity). The platform is the host executable’s, which can differ from the machine’s, as for an x64 build under emulation. Without one of them run exits with 2 before any case.
--reportThe report file to write.
--deviationsA JSON file {"deviations": [{"caseId", "tracking", "note"}]}. A deviation is attached to its case in the report; it never changes an outcome.
--guest-kit-report and its two digestsA passing generic guest kit run through the same executable, cited by the report. The cited host digest must be the --host digest.

Outcomes

OutcomeMeaning
passedEvery run satisfied every matcher of its expectation. For a run with alternatives, one alternative whose condition matches the host is enough, and the report names it in satisfied: the run, the alternative’s number counted from 1 in corpus order, and its condition. For a runaway case this says which bound ended the call.
failedA run satisfied no expectation that applies to the host. The report names, for each failure, the run, the alternative (null for a run without alternatives), the matcher and why; a run with alternatives lists the failures of every applicable alternative.
errorThe runner could not judge the case, for example because no started event declared a bound the case depends on. Never a pass.
not_applicableThe case needs a profile the host does not declare.
not_enforcedThe host’s started events declare a bound of the case not enforced.
refusedThe module’s bytes differ from the recorded digest; no worker started.

Exit codes

Coderunreport-check
0Every applicable case passed. A not_enforced case passes only for the instruction budget; the memory ceiling and the call deadline are required.The report is the kit’s and every recomputed outcome passes.
1A case did not pass, or an operational error, such as a host that cannot give its build identity.The report is invalid, or a recomputed outcome does not pass.
2An invalid invocation: a missing option, the offering, platform and engine among them, a host that is not a file, an unknown profile, or a kit of another version.An invalid invocation: a missing option or report, or a kit of another version.

Output

stdout carries one JSON document: the command, "experimental": true, the exit code, the report path and digest, and the totals by outcome. Progress and diagnostics go to stderr.

{"command":"mck wasm-host run","experimental":true,"exitCode":1,"report":"/work/.dev/wasm-host.json","reportSha256":"…","host":"/opt/morphir/bin/morphir","hostSha256":"…","totals":{"cases":59,"passed":57,"failed":2,"error":0,"refused":0,"notApplicable":0,"notEnforced":0},"qualificationNotice":"…"}

The report (morphir/mck-wasm-host-report 0.1.0-draft.2) is indented JSON with a fixed member order, so the same observations give the same bytes. It records the suite and corpus pin, the contract version, the runner, the host executable’s path, arguments and SHA-256, its build identity, profiles, offering, platform and engine, what the started events declared about each bound, and for each case its module digests, outcome, the alternatives it satisfied or its failures, and the observation of each run: the worker arguments, stdout (base64), every stderr line with the time it was read, the exit status and the times. report-check refuses a report that does not identify the host’s offering, platform and engine.

Examples

# The MoonBit native CLI, which also runs the byte profile.
morphir mck wasm-host run --kit conformance/suites/wasm-host \
  --host /path/to/morphir --host-profile morphir-bytes-v1 \
  --offering native-cli --platform win-x64 --engine "moonrockz/extism 0.3.0" \
  --report .dev/wasm-host.json

morphir mck wasm-host report-check --kit conformance/suites/wasm-host --report .dev/wasm-host.json

# A .NET executable, here the Native AOT morphir. It runs extism-kernel-v1 only, so no --host-profile.
morphir mck wasm-host run --kit conformance/suites/wasm-host \
  --host artifacts/executables/win-arm64/morphir.exe \
  --offering native-aot --platform win-arm64 --engine "wasmtime 50.0.1" \
  --report .dev/wasm-host-native-aot.json
  • conformance/README.md: the suite and its generator.
  • tests/Fixtures/MepWasm/: the pinned contract and corpus.

7.11 - morphirup

Set up the Morphir toolchain with morphirup and run it, then the command reference

morphirup

morphirup installs the Morphir command line, morphir, with the F# frontend and backend it uses, and runs them. It is one executable and needs no .NET. For a walkthrough with real output, see Your first model with morphirup.

Acquire the Morphir CLI and set up a toolchain

PlatformInstall morphirupsetup, show, select and run
Windows x64 and ARM64YesYes
macOS on Apple siliconYesYes
Linux x64 and ARM64YesNo: setup doesn’t support Linux yet

Intel Macs are not supported.

  1. Install morphirup, as Installation shows.

  2. In your project folder, see what setup would install, then install it:

    morphirup setup --dry-run
    morphirup setup --yes
    

    The dry run checks the release and installs nothing. setup --yes downloads the Morphir CLI, the F# frontend and the F# backend from the release of the same version as morphirup, checks them against the release’s checksums, tries them out, and selects them. This is the only step that needs the network.

  3. See what is installed:

    morphirup show
    

    "status": "shown" means a toolchain is set up and selected.

  4. Switch to another installed toolchain when you need to:

    morphirup select --generation <generation-sha256> --dry-run
    morphirup select --generation <generation-sha256>
    

    Each setup --yes installs a new copy, called a generation, and selects it. Older generations stay installed. setup and show print the generationSha256 of the one they set up or show. select checks every file of the generation you name, then selects it, for example to go back after a newer setup. use is the same command.

  5. Run the CLI. Everything after -- goes to morphir:

    morphirup run -- compile
    morphirup run -- generate --target fsharp --output generated
    

    run uses only what is installed, so it works offline.

morphirup keeps everything in one folder, the Home: MORPHIR_HOME if it is set, and .morphir in your user folder otherwise. setup also takes --home <dir>, and show, select and run take --home <absolute path>.

When something goes wrong, see Troubleshooting.

How release setup works

morphirup is the standalone Morphir toolchain manager of the approved change add-deployment-and-acquisition (finos/morphir, decisions UP-1, UP-2 and LOCK-1). It is a Native AOT executable. Release setup is experimental. Historical recipe setup remains plan-only. Explicit local host installation, selection and execution remain available through the local candidate instructions. Publisher authentication, portable lock restore, proxy installation and shell setup remain unfinished.

Setup uses its own release version unless .morphir/toolchain.toml requests an exact version. It downloads the release’s SHA256SUMS and the bundle file of its own platform (morphir-toolchain-<rid>.json), then the runtime-bundled Morphir host and independent F# frontend and backend that the bundle names. It checks their byte counts and SHA-256 values, installs their complete folders, executes the fixed readiness workflow, and selects the completed generation last. --home is optional and defaults to MORPHIR_HOME, then ~/.morphir. A dry run reads only SHA256SUMS and the bundle, and creates no Home, locks, staging, receipts or selection and runs no guest. Setup refuses before those effects if the final host or provider executable path would exceed the Windows launch limit of 259 characters. Choose a shorter --home when the diagnostic names this limit.

Setup normally removes its own private staging folders. If the mutation lease stays busy for five seconds, cleanup leaves that folder and writes one warning on stderr without changing setup’s achieved result. A later setup does not sweep deferred or interrupted stages; that follow-up is tracked in #617.

This is release-checksum delivery, not publisher-authenticated. HTTPS and a matching release checksum do not authenticate the publisher. The release transport starts only at the exact finos/morphir-dotnet repository and version tag. It permits at most three HTTPS redirects to github.com, release-assets.githubusercontent.com or objects.githubusercontent.com, without credentials or a downgrade. The generic acquisition transport keeps its existing no-redirect policy.

For composed generations, run selects the installed F# frontend when compile --language fsharp is explicit, and the installed backend for generate --target fsharp. Without --language, the host resolves project configuration; the manager does not force F# over another language. Explicit --extension or --extension-id selections are passed through unchanged.

After setup succeeds, the same run commands use installed folders and retained selected records without network access or the download cache. The existing explicit local candidate commands remain diagnostic entry points. This scope does not implement publisher authentication, trust commands, moving release channels or portable lock restore. The manual Windows released morphirup journey workflow validates the actual published executable after assets are available.

Each generation’s receipt, toolchains/generations/<generation-sha256>/receipt.json in the Home, names the host’s implementation, deployment, platform, version and entry point. toolchains/default.json records the selected generation, and catalog/extensions.json lists the installed providers. select, show and run read only the current local-toolchains family; in a Home set up by an earlier family they refuse with local.setup_required, and setup --yes installs the current family beside the earlier generations.

On macOS

On macOS on Apple silicon, the installed host and providers are the osx-arm64 builds, with the entry points morphir, morphir-fsharp-frontend and morphir-fsharp-backend. Intel macOS (osx-x64) is not supported: an x64 manager under Rosetta refuses setup and run. The executables are not notarized.

  • Setup keeps the bytes of each executable as the SDK published them. The SDK signs each apphost ad hoc, and ARM64 macOS runs only a signed executable, so the signature stays valid.
  • A host file is installed with mode 0755 when its archive mode is executable, as for the entry point and createdump, and 0644 otherwise. A provider file is installed read-only, 0555 or 0444. Setuid, setgid and group or other write bits in an archive are never installed.
  • Setup uses the physical path of the parent folders of Home. On macOS, $TMPDIR is below the link /var, and the installed host refuses a source file below a link. A Home that is itself a link is refused.
  • run starts the host with the same environment as on Windows: MORPHIR_HOME, MORPHIR_ISOLATED=1, the discovery boundary and DOTNET_MULTILEVEL_LOOKUP=0, and no DOTNET_ROOT, DOTNET_ROOT_X64, DOTNET_ROOT_ARM64, DOTNET_SHARED_STORE, DOTNET_ADDITIONAL_DEPS or DOTNET_STARTUP_HOOKS.
  • Process ownership is weaker than on Windows. On Windows the host runs in a kill-on-close job: when the host exits, morphirup stops each process that is still in the job, and when morphirup dies, the job stops them all. On macOS the host has no process group of its own. When you cancel run (Ctrl+C) or its output fails, morphirup stops the host and the processes below it that are still its descendants at that time. It does not stop a process that the host started and left running when the host exited normally, and it stops nothing when morphirup itself is killed.

To build and install the macOS ARM64 inputs from a checkout, see macOS ARM64 local candidate.

Download and verify

Releases of morphir-dotnet attach morphirup for five platforms, beside the morphir assets on the releases page:

PlatformExecutableArchive
Linux glibc x64morphirup-linux-x64morphirup-linux-x64-v<version>.tar.gz
Linux glibc ARM64morphirup-linux-arm64morphirup-linux-arm64-v<version>.tar.gz
Windows x64morphirup-win-x64.exemorphirup-win-x64-v<version>.tar.gz
Windows ARM64morphirup-win-arm64.exemorphirup-win-arm64-v<version>.tar.gz
macOS ARM64morphirup-osx-arm64morphirup-osx-arm64-v<version>.tar.gz

A Linux executable needs glibc 2.35 or later from the machine, which Ubuntu 22.04, Debian 12 and later releases of both have, and nothing else: no .NET, no ICU and no OpenSSL. It does not run on musl distributions such as Alpine. The macOS executable needs macOS 12.0 or later: that is the minimum its LC_BUILD_VERSION load command declares, as the release job logged for v0.4.0-alpha.4. The job checks the executable only on the runner’s macOS (macOS 26), not on macOS 12.

The Windows ARM64 executable is built and checked on the GitHub-hosted Windows 11 ARM64 runner, natively, not cross-built on x64. Releases before the first one that lists it have no win-arm64 files. The release workflow publishes the complete release-setup bundle for Windows x64 and Windows ARM64, with one SHA256SUMS over both.

Each archive holds the same executable under <rid>/, for example linux-x64/morphirup, with the repository’s LICENSE and NOTICE and the executable’s THIRD-PARTY-NOTICES.txt beside it. The release also carries that file as morphirup-v<version>-THIRD-PARTY-NOTICES.txt, for the bare executables: it lists the packages, the .NET runtime and the Unicode data that Native AOT compiled in, with their licenses and notices. The file morphirup-v<version>.sha256 lists the SHA-256 of every morphirup asset of the release, the notices file included, in the format that sha256sum -c reads. It shows that the file you have is the file the release published. It is not a signature: verifying signed release metadata is later work.

On Linux:

version=0.4.0-alpha.5   # the release you want
base=https://github.com/finos/morphir-dotnet/releases/download/v$version
curl -fLO "$base/morphirup-linux-x64"
curl -fLO "$base/morphirup-v$version.sha256"
sha256sum -c --ignore-missing "morphirup-v$version.sha256"
chmod +x morphirup-linux-x64
./morphirup-linux-x64 --version

On macOS, compare the digest with its line in the checksum file:

shasum -a 256 morphirup-osx-arm64
grep ' morphirup-osx-arm64$' "morphirup-v$version.sha256"

On Windows, in PowerShell:

(Get-FileHash .\morphirup-win-x64.exe -Algorithm SHA256).Hash.ToLowerInvariant()
Select-String -Path ".\morphirup-v$version.sha256" -Pattern ' morphirup-win-x64.exe$'

The release job checks each executable on a GitHub-hosted runner of its platform. It runs the published file from a folder whose path has a space, with PATH reduced to the system folders and no DOTNET_* variable. The file must print the release version and make a plan-only setup whose plan digest matches a recorded one.

A Native AOT executable needs the glibc of the machine that linked it, or newer. So the Linux executables are linked in an ubuntu:22.04 image pinned by digest, which has glibc 2.35, and not on the runner. The release job reads the glibc versions each Linux executable needs (readelf --version-info) and fails if one is above 2.35. It then runs the same commands in debian:12-slim (glibc 2.36) and in the ubuntu:22.04 build image (glibc 2.35), after checking that neither image has .NET, libicu, libssl or libcrypto. ubuntu:22.04 ships OpenSSL (libssl3), so the check removes that package from a copy of the image first. It also checks that each archive holds exactly the four files above and that the notices name every package the compiler linked. Nothing more is checked: no other distribution, glibc version or operating-system version is tested.

The executables are not code-signed, and the macOS executable is not notarized. On macOS, a file that a browser downloads carries the quarantine attribute, and Gatekeeper refuses to open it because Apple cannot check it for malicious software; a file that curl downloads has no quarantine attribute. After you have checked its digest, you can remove the attribute with xattr -d com.apple.quarantine morphirup-osx-arm64. On Windows, SmartScreen can warn about an unsigned file that a browser downloads. Neither behavior has been checked for these files on a Mac or a Windows desktop.

The Windows and macOS ARM64 executables also set up and run a toolchain, as above. No release verifies signed metadata yet.

Implementation, deployment and toolset

A request names three things, and the manager never infers one from another:

  • the implementation is which Morphir host, such as dotnet;
  • the deployment is how that host is delivered: native-aot, self-contained, framework-dependent or dotnet-tool. Each is a separate offering with its own artifacts, so native-aot is never replaced by self-contained;
  • the toolset is the preset of language operations, such as fsharp, which needs F# compilation and F# generation. Each operation gets one provider: the host’s built-in, or an independently released process, wasm or dotnet-tool provider.

The project request is .morphir/toolchain.toml:

schemaVersion = "0.1.0-draft.1"
[host]
implementation = "dotnet"
deployment = "native-aot"
channel = "stable"
[toolset]
preset = "fsharp"

It is a separate file from morphir.toml. The [toolchain.<name>] task adapters in morphir.toml keep their meaning, and morphirup never reads or writes them.

Check in both .morphir/toolchain.toml and .morphir/toolchain.lock.json. The lock records the exact host and providers for each platform, with no local paths. Its requestSha256 is the SHA-256 of the request file’s exact bytes, so reformatting the request, even adding a blank line, needs a new lock. A colleague on another platform will extend the lock with an explicit setup for that platform; this build does not implement that yet, and morphirup setup --plan --platform <rid> only shows the plan. Frozen restore never resolves a channel again.

Commands

setup --plan (or setup --dry-run) and doctor write one manager result document (0.1.0-draft.1, see the contract, a draft in the unmerged finos/morphir#1147, at the commit this build is pinned to) to standard output and their progress to standard error. --version, --help and a command line that cannot be parsed write no result document. JSON is the only output format of this build; --json is accepted.

Exit codeMeaning
0The result is planned or succeeded
1The result is failed: a document was refused or a check failed
2morphirup did not run the command; see Exit code 2. Nothing is written to standard output; the reason is on standard error
3Provisional. Internal error. Nothing is written to standard output. The result contract defines exit codes 0, 1 and 2 only

Exit code 2

Exit code 2 means that morphirup did not run the command. That happens when:

  • no command is given, or the command is not setup or doctor;
  • an option is unknown, given twice, or has no value. A value never starts with --, so --project --plan is a missing value, not a folder named --plan;
  • setup is run without --plan or --dry-run, or with --yes;
  • --platform is not one of the eight platform ids, or this machine is not one of them and --platform is not given;
  • the value of --repository-id, --expect-plan, --implementation, --deployment, --preset, --channel or --version is not valid;
  • setup has no --repository-id or no --recipe, or doctor has --recipe without --repository-id;
  • the request is given two ways: --request with --project or with the selector options, or --project with the selector options;
  • the selector options are incomplete: --implementation, --deployment or --preset is missing, or not exactly one of --channel and --version is given;
  • the --artifact-sizes file cannot be read, is not a JSON object of objects, or has a size that is not a non-negative integer;
  • a downloaded artifact has no size in --artifact-sizes. setup finds this only after it has read the request and recipe and resolved the plan.

A request, lock or recipe that is absent or cannot be read is not exit code 2: the command runs and fails with document_unavailable (unsupported-contract, naming the document, at the empty path), exit 1.

Digests, repository ids and bounds

A plan carries the lock that setup would write, and the lock’s requestSha256 is the SHA-256 of the request file’s exact bytes. So a checkout that converts .morphir/toolchain.toml to CRLF line endings, or adds a byte order mark, gives another planSha256, and --expect-plan then fails with plan_changed. plan_changed names no cause. morphirup doctor does: its request_changed diagnostic says when line endings or a byte order mark are the likely cause.

In this build, --repository-id is copied as given into the plan’s source.repositoryId values and the lock’s recipe.repositoryId, and nothing checks it. The plan’s repository-target and recipe-digest sources label where each artifact would come from. They are not verified provenance: this build downloads and authenticates nothing.

A request, lock or recipe nested deeper than 64 levels is refused as invalid_document, whether the nesting is in JSON or comes from TOML arrays, inline tables or dotted keys.

A version, in a document, a comparator or --version, is at most 256 characters, and each numeric identifier (major, minor, patch, or an all-digit prerelease identifier) has at most 15 digits, as the contract states. A longer one is refused as invalid_value at its member before it is parsed. That holds for a MEP version (protocolVersion, protocolVersions) too: any text that is not a version in canonical SemVer spelling, such as 0.1, v0.2.0 or a patch of 16 digits, is invalid_value, and only a SemVer version off the supported 0.2 line, such as 0.1.0 or 0.3.0, is unsupported_case.

Files

morphirup reads a request, lock, recipe or sizes file only when it is a regular file. A directory, device, pipe or socket is refused, and so is a symbolic link to one, including a link into /dev, /proc or /sys. A file has five seconds to open, and then thirty seconds for each read: a file that keeps sending data is read in full, and one that sends no data for thirty seconds is refused as “is not a regular file: it sent no data for thirty seconds”. A refused request, lock or recipe is document_unavailable; an unreadable sizes file, which is this build’s own input, is exit 2.

A read error is reported in morphirup’s own words, never as a system message: “does not exist”, “cannot be read: it is in use by another process”, “cannot be read: access is denied”, “cannot be read: its path is too long for this system”, or “cannot be read: input or output failed” with the error code.

The size bounds are provisional, until the contracts state their own:

FileLargest size read
Request (.morphir/toolchain.toml or --request)1 MiB
Lock (.morphir/toolchain.lock.json or --lock)1 MiB
--artifact-sizes1 MiB
--recipe16 MiB

A larger file is refused before it is parsed.

Finding the project

Without --request or the selector options, morphirup finds the project request by discovery: the nearest .morphir/toolchain.toml in the start directory or one of its parents, up to and including the discovery boundary. Nothing above the boundary is read, and requests are never merged, so a workspace member with its own request uses it and a member without one uses the workspace root’s. The start is --project <dir>, or the current directory; the boundary is --boundary <dir>. A start outside the boundary has no project request. --project and --boundary are provisional.

Discovery stops at the first level where .morphir/toolchain.toml exists in any form: a file, a directory, a link (even one that points nowhere) or an entry that cannot be listed. Anything there that is not a readable regular file is refused as document_unavailable, and the request of an ancestor is not read instead.

Without --boundary, discovery searches up to the root of the file system: / on Linux and macOS, the drive’s root on Windows. So when no nearer request exists, a .morphir/toolchain.toml in any ancestor of the start directory is used, for example in your home directory, in /tmp or at the root of a shared drive. Give --boundary <dir> to stop at your workspace. Give --boundary in the same letter case as the path you start from: on Windows the comparison ignores case; elsewhere it is exact, even on a case-insensitive volume.

doctor reads the lock beside the request: the one it found, or the file given with --request, unless --lock names another.

Dry runs

Every morphirup command that creates, updates, repairs or removes persistent state has a --dry-run option. A dry run takes the same planning and validation path as the command itself. It refuses whatever the command would refuse, with the same codes, paths and exit codes, and it makes no persistent change: it writes no request, lock, receipt, journal, cache, home or activation file, downloads nothing and runs no readiness probe. Its result reports only what is true: an action it would take is planned, never succeeded, and an action it cannot complete is reported with the diagnostic that blocks it.

In this build, only setup could change state, and it only plans, so setup --dry-run is setup --plan under another name (see below). doctor reads and never changes anything, so it has no --dry-run. A future doctor --fix, and every later command that installs, activates, updates, rolls back, repairs or removes, will come with --dry-run from the start; none of them is implemented yet.

morphirup --version

Prints the version and nothing else.

morphirup setup --plan and --dry-run

morphirup setup (--plan | --dry-run) --recipe <file> --repository-id <sha256> --artifact-sizes <file>
                [--project <dir> [--boundary <dir>] | --request <file> | <selector options>]
                [--platform <rid>] [--expect-plan <sha256>] [--recipe-target <path>]

Reads the request, resolves it against the recipe for every platform the lock would record (the request’s platforms plus the platform being set up), and prints a planned result with the plan and its planSha256. The plan names the host artifact, each provider, where each comes from, its sizes, its update owner, the prerequisites that apply on its platform and its probe. It also carries the complete lock setup would write, with every platform’s closure, so planSha256 pins all of it. The host and every process or dotnet-tool provider are process-with-user-rights: setup’s readiness checks would run that code with your rights. A built-in or WASM provider is in-host. Planning runs nothing.

Plan-only setup is the only setup in this build: without --plan or --dry-run, or with --yes, it exits with code 2 and changes nothing. Installing is not implemented yet.

--dry-run is the same request as --plan: the same plan and planSha256, the same result bytes, the same refusals and the same exit codes. Both may be given together and mean the same; each may be given once. Neither permits --yes, so a dry run can never install or activate anything. A dry run reads only the request, recipe and sizes files, and creates, changes or removes nothing in the project or the Morphir home.

OptionMeaning
--project <dir>Provisional. Where discovery starts. The default is the current directory
--boundary <dir>Provisional. The discovery boundary. The default is the root of the start path
--implementation, --deployment, --preset, --channel or --versionThe request as options, as in the approved design. The approved example given this way has the approved example file’s identity
--platform <rid>Plan for this platform instead of the current one
--expect-plan <sha256>Refuse with a failed result (exit 1) when the plan’s digest differs: plan_changed at planSha256, naming both digests. Nothing is changed
--recipe <file>Provisional. The recipe document. Later builds take it from authenticated TUF metadata; this build trusts the file you give it
--repository-id <sha256>Provisional. The repository that publishes the recipe and the host artifacts. A recipe provider bound to another repository is repository_mismatch, a trust-failure diagnostic whose document and path name that provider’s repositoryId
--recipe-target <path>Provisional. The recipe’s target path, by default toolchains/recipes/<preset>/<version>.json. A target that is not portable, such as one with a con segment, is non_portable_path before the recipe is read
--artifact-sizes <file>Provisional. A JSON object from target path to {"downloadBytes": n, "installedBytes": n}. A plan needs both sizes for every downloaded artifact. Later builds take downloadBytes from the authenticated target length; the contract has not settled where installedBytes comes from
--request <file>Provisional. Read this request file

A request the recipe cannot satisfy gives a failed result with an unsupported-artifact diagnostic, whose document (request) and path name the request member it concerns, such as toolset.providers[1]. The manager never substitutes another implementation, deployment, release, platform or WASM ABI. A missing platform lists the recipe’s alternatives; a WASM provider whose ABI the host lacks is abi_unavailable unless the request chooses another ABI explicitly.

morphirup setup --plan --recipe fsharp-1.0.0.json --repository-id <sha256> --artifact-sizes sizes.json
morphirup setup --dry-run --recipe fsharp-1.0.0.json --repository-id <sha256> --artifact-sizes sizes.json
morphirup setup --plan --implementation dotnet --deployment native-aot --preset fsharp --channel stable `
    --recipe fsharp-1.0.0.json --repository-id <sha256> --artifact-sizes sizes.json --expect-plan <sha256>

morphirup doctor

morphirup doctor [--project <dir>] [--boundary <dir>] [--request <file>] [--lock <file>] [--platform <rid>]
                 [--recipe <file> --repository-id <sha256>]

Checks a project’s request and lock without changing anything:

  • both documents can be read (document_unavailable otherwise) and read as documents;
  • the lock binds the exact request bytes, hashed as read (request_changed otherwise). When the bytes differ only by CRLF line endings, a UTF-8 byte order mark or both, the diagnostic’s cause says so and its message names the .gitattributes entry, .morphir/toolchain.toml text eol=lf, that prevents it. A converted file is never accepted;
  • the lock records the current platform (platform_not_locked otherwise). An explicit setup for that platform will extend the lock; this build does not implement it yet. The diagnostic says so and names only morphirup setup --plan --platform <rid>, which shows the plan;
  • with --recipe and --repository-id, the lock names that repository (repository_mismatch otherwise, with document lock and the path of the member that names another one) and every closure is one the recipe allows (lock_recipe_mismatch). --recipe needs --repository-id, because the lock-against-recipe check takes the repository as an input.

A recipe that is refused is reported even when the lock is refused too. The result is succeeded with no diagnostics, or failed with every diagnostic found. Installed generations are not checked yet. --project, --boundary, --request, --lock, --recipe and --repository-id are provisional.

Recipe binding checks

Morphir.Toolchain can check a recipe’s bindings against the repository that authenticated it: every host and provider row of the toolset recipe page’s authenticated recipe targets. It reads three documents for this, each at the one version the toolchain contract lists, and no earlier one:

  • the tool release metadata v1 release descriptor of a tool-release host offering (schemaVersion 1);
  • the extension release descriptor a provider binds (schemaVersion 2.0.0-draft.2);
  • the capability claim set of each of its artifacts (claimsVersion 0.1.0-draft.2).

It also selects a tool release by the tool release metadata v1 rules for channels, CLI compatibility and platforms.

No morphirup command uses any of this yet. Nothing downloads anything, and nothing here verifies repository metadata: the checks take the authenticated targets and their bytes as inputs. This code has not had a security review. It must not be used to authenticate a production release until it has.

A provider’s claims say what its publisher claims the artifact does. Checking that they name the provider’s capability, MEP version, language or target and model tuple is not running the artifact. A recipe digest authenticates the bytes; the descriptor’s claimCheck says whether anyone ran them to read the claims. The two are kept apart, and a recipe never changes claimCheck.

Platforms and prerequisites

morphirup uses the .NET runtime identifiers win-x64, win-arm64, osx-x64, osx-arm64, linux-x64, linux-arm64, linux-musl-x64 and linux-musl-arm64. A musl machine never gets a glibc closure. These are the platforms it can plan for; releases publish the executable for five of them, listed under Download and verify.

The executable needs no .NET runtime, no ICU and no OpenSSL: it is built in invariant globalization mode, it normalizes strings for the plan digest to NFC with its own Unicode 17.0.0 tables, and it computes SHA-256 with BouncyCastle’s managed implementation. So it runs on a Linux machine without the libicu or libssl packages, and a plan digest is the same whatever Unicode version the machine has.

Common errors

For problems with installing, setup, show and run, see Troubleshooting. The errors below come from recipe planning (setup --plan) and doctor.

  • this build plans only: pass --plan or --dry-run, without --yes. Installing is not implemented yet.
  • document_unavailable: the named request, lock or recipe does not exist, is over the bound in Files, or is not a regular file. Without --request, check where discovery looked.
  • request_changed with cause crlf-line-endings: Git converted the request’s line endings. Add .morphir/toolchain.toml text eol=lf and .morphir/toolchain.lock.json text eol=lf to .gitattributes and check the files out again.
  • the size of <target> is unknown: add the target to the --artifact-sizes file.
  • unsupported_contract_version at schemaVersion: this build reads 0.1.0-draft.1 documents only, never another draft or the released 0.1.0.
  • credential_in_document: no toolchain document may hold a member such as token, password or apiKey, at any depth and in any letter case. The result names the member, never its value.

Repository metadata verification

Morphir.Toolchain has the verification part of a TUF client for the tool release metadata v1 profile: it reads root, timestamp, snapshot and targets metadata, checks Ed25519 signature thresholds, follows root rotation, and checks rollback, links and expiry. No morphirup command uses it yet, and nothing in this release downloads or authenticates repository metadata. This code has not had a security review. It must not be used to authenticate a production release until it has.

A trust anchor has a purpose, TestOnly or Production, and every root the client later accepts by rotation must meet the same rules as the anchor. A production anchor must be the repository whose identity the caller names: no root is production on the strength of the checks below alone, and no production identity ships with this build. The production checks then refuse:

  • a root threshold below two, at the anchor and at every rotation;
  • any of the five public keys of this repository’s TEST-ONLY signed fixture, wherever a root lists them.

That deny list stops those five keys and nothing else. It does not stop any other published test seed, of which this repository has several, and it says nothing about which repository is meant. That is why production also needs the repository identity.

7.12 - morphir extension worker (experimental)

Experimental and internal: serve one WASM extension module over stdio for a conformance runner

morphir extension worker

Experimental and internal. The worker exists so that a conformance runner can qualify this host’s WASM runtime with the crafted cases of the WASM execution profile contract (OpenSpec change add-wasm-extension-host, task 4.1). Product commands do not use it, and it is not in the help of morphir extension. Its arguments and events follow the pinned contract, which is still a draft, so they can change in any release. Contributors: see WASM host qualification.

Synopsis

morphir extension worker --abi <profile> --module <path> --build-identity <hex>
    [--request-bytes <n>] [--response-bytes <n>] [--call-ms <n>] [--instructions <n>]
    [--memory-pages <n>] [--json]
morphir extension worker --help [--json]

Both executables have it: morphir and dotnet-morphir, and so does the managed morphir built from src/Morphir.Managed, which is not published yet. Each starts the worker before it builds the rest of the command line, so the worker starts fast and nothing but MEP frames reaches stdout.

Description

The worker loads one module and relays MEP messages to it. Each Content-Length framed message on stdin is one handle call of the module. The reply to a request is written as one frame on stdout. A notification gets no frame, and reaches the guest only if the guest asked for notifications in its morphir.initialize result. The worker does not negotiate the protocol, and does not check the JSON-RPC version or ids: the runner at the other end is the MEP host.

The worker keeps reading until the module’s instance ends, at morphir.exit or at a fault, and then exits by itself. A runner keeps stdin open until then.

Before it serves, the worker checks the build identity (before the module file is read), then loads the module: it reads the file, computes its SHA-256, validates and compiles it, classifies it against the pinned contract and admits it with the declared ABI. Then it opens one instance under the bounds and writes started.

Options

OptionDefaultMeaning
--abi <profile>requiredThe declared profile, extism-kernel-v1 or morphir-bytes-v1. This host runs extism-kernel-v1 only and refuses a morphir-bytes-v1 module. A declaration only confirms the profile the imports give.
--module <path>requiredThe module file. A relative path is resolved against the current directory.
--build-identity <hex>requiredThis executable’s build identity, 64 hexadecimal digits, as --help --json reports it. Without it the worker serves nothing and exits with 2. Another identity is refused with extension.worker_runtime_mismatch.
--request-bytes <n>6291456 (6 MiB)The largest request frame, its Content-Length header included. A larger frame is fault failure, and the guest is not called.
--response-bytes <n>16777216 (16 MiB)The largest response frame, header included. A larger response is fault failure, and nothing of it reaches stdout.
--call-ms <n>30000 (10000 for morphir-bytes-v1)The deadline of each handle call, in milliseconds.
--instructions <n>5000000000The instruction budget of each call, in Wasmtime fuel units, from 1 to 9007199254740991, the range the contract states.
--memory-pages <n>4096 (256 MiB)The ceiling of the guest’s memory, in 64 KiB pages, from 1 to 65536. This host has one limit for every memory of a session, so it applies to the Extism kernel’s memory too, as the contract allows. Below the default the two hosts differ: the MoonBit host leaves the kernel at 256 MiB. A value outside the range is a usage error, exit 2, before the module is read.
--jsonoffWrite the report as morphir/command-event lines.
--help, -hWrite the help on stdout and serve nothing. With --json, write one morphir/command-result whose result.buildIdentity is the identity to pass.

The defaults and the range of --instructions are the pinned contract’s (tests/Fixtures/MepWasm/wasm-execution-profiles.json, worker.arguments). The contract states no other range; the others are this host’s own.

The build identity

The identity is the SHA-256 of a short text that names the pinned profile contract (id, version and SHA-256), the host (morphir or dotnet-morphir), the release version with its build metadata (the git commit of a CI or release build), the embedded Extism kernel (moonrockz/extism version and SHA-256) and the engine (wasmtime 50.0.1). Two builds of one version and commit share an identity, also when one of them has uncommitted changes.

$ morphir extension worker --help --json
{"profile":"morphir/command-result","contractVersion":"0.1.0","command":"extension worker","state":"succeeded","exitCode":0,"result":{"help":"...","buildIdentity":"e4d9...531d"},"diagnostics":[]}

Output

Stdout carries only Content-Length framed replies, in every case.

Stderr carries the report. With --json each event is one line:

{"profile":"morphir/command-event","contractVersion":"0.1.0","command":"extension worker","sequence":2,"event":"fault","fault":{"kind":"trap","message":"extension.guest_trap: the guest trapped: ..."}}
EventMembers after the envelopeWhen
startedabi, moduleSha256, buildIdentity, boundsThe module has loaded. bounds has exactly memoryCeiling, instructionBudget and callDeadline, each {"enforced": true}: whether the bound is enforced, not its value.
outputoutput; or output and droppedBytesGuest output of the call that just ended: WASI stdout and stderr, Extism and Morphir log messages, refused HTTP requests. The newest 64 KiB of a call are reported, each message cut to 4 KiB, then one event with droppedBytes for the rest.
faultfault.kind, fault.messageThe session failed. The kind is trap, memory-limit, budget-exhausted, call-deadline or failure. The message states the guest’s memory size, the kernel’s memory size and the ceiling. A fault has no code.
refusederror.code, error.messageA refusal before any request: extension.worker_runtime_mismatch, extension.runtime_unavailable, extension.abi_unsupported, extension.abi_mismatch or extension.wasm_feature_unsupported. The message of a module refusal names the file and its SHA-256.
exitedexitCodeLast, in every run that got past the arguments.

The envelope is profile, contractVersion, command and sequence (from 1), then event. A message in an event is cut to 4 KiB of JSON text and then ends with ….

No event carries the engine identity, with or without --json. The engine is part of the build identity, and a runner is given it as an input; session diagnostics and the runtime report state it. The contract fixes the event names and their members. The rest of the morphir/command-event and morphir/command-result documents, the help text and the usage-failure document follow the MoonBit CLI until a shared contract states them.

Without --json each event is one text line that starts with morphir extension worker: , for example morphir extension worker: serving extism-kernel-v1 module 013d...e11e.

A usage error serves nothing. With --json it is one morphir/command-result failure on stderr (error.code usage); without it, one text line on stderr.

A host without its WASM runtime

When this executable cannot run WASM modules, the worker checks that after the arguments and the build identity and before it reads the module. It writes one refused event with extension.runtime_unavailable, whose message names the host, the platform and the reason, writes nothing on stdout and exits with 3. --help and the identity check never load the engine. The reasons are those of morphir extension runtimes. The pinned profile contract lists the code among the worker’s refusals, and its worker rule start-order fixes this order: the arguments, the build identity, a usable engine, then the module.

Faults before started

A module file that cannot be read, or that is not a well-formed WASM module, is fault failure before started. So is a module whose initial memory is above the ceiling, with kind memory-limit. Each exits with 1. None of them is a refusal.

Message checks

The worker checks that each request and each response is UTF-8 text holding one JSON object. Anything else is fault failure; a request that fails the check never reaches the guest, and a response that fails it never reaches stdout.

Interruption

Ctrl+C (SIGINT) stops the guest within one second, even inside a call that spins. The worker writes no fault event, because the guest did not fail, and exits with 130. On Windows Ctrl+Break does the same, because a parent can send only Ctrl+Break to a child that runs in its own process group.

Private workspace

A guest that calls morphir_get_workspace_info gets an empty private directory. The worker creates its own directory in the system’s temporary directory (morphir-extension-worker-<hex>), the session creates the guest’s directory below it, and the worker removes both when it exits, also after a fault or an interruption.

Exit status

StatusMeaning
0The guest served until its instance ended.
1A fault, an invalid frame, or stdin ended first.
2Invalid arguments.
3A refusal before any request.
130Interrupted.

Example

$ id=$(morphir extension worker --help --json | jq -r .result.buildIdentity)
$ morphir extension worker --abi extism-kernel-v1 --module ./extism-echo.wasm --build-identity "$id" --json

7.13 - Troubleshooting

Solutions to common issues with Morphir .NET CLI

Troubleshooting Guide

C# generation

Use exact target csharp, numeric language-version=14 or 15, and an optional literal C# namespace. These invocations fail with exit 2 before publication:

morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target csharp --output artifacts/generated-csharp --option language-version=preview --json
morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target csharp --output artifacts/generated-csharp --option namespace=true --json

The first requires a numeric profile. The second supplies a JSON Boolean where a namespace string is required. Use --option language-version=14 and --option namespace=Pricing.Generated. Source syntax, empty segments and unescaped reserved words are refused with a located namespace diagnostic. Use @ for reserved words, for example namespace=@class.Models.

In JSON mode, parse the single stdout report and inspect failureKind and diagnostics. Structured diagnostics are report data; JSON failures need not duplicate their text on stderr. Without --json, human diagnostics use stderr. Exit 1 identifies unsupported or invalid model semantics; exit 2 identifies invocation or operational failures; cancellation before publication returns 130. No successful partial sources are published for these failures.

An unsupported expression, SDK operation, distribution or required semantic has a stable rule and available model location. Read the C# API/diagnostic guide, then correct the input or restrict it to the admitted model contract. Do not edit emitted sources to hide a refusal. For a compiler error, verify the separately selected compiler’s numeric language version and the Morphir.SDK.CSharp package reference using the fresh-consumer walkthrough.

Keep input and output separate and retain .morphir-generated.json. A failed generation or rejected overlap preserves earlier owned sources and unowned files. Shared publication does not promise rollback after a filesystem failure during final commit; inspect the report and rerun after fixing that failure.

This guide covers common issues you may encounter when using the Morphir .NET CLI and how to resolve them.

Installation Issues

Tool Not Found After Installation

Problem: After running dotnet tool install, the morphir command is not found.

Solution:

  1. Check if tools are in PATH:

    echo $PATH | grep .dotnet/tools
    
  2. Add to PATH if missing (Linux/macOS):

    export PATH="$PATH:$HOME/.dotnet/tools"
    
  3. Add to PATH if missing (Windows):

    $env:PATH += ";$env:USERPROFILE\.dotnet\tools"
    
  4. Restart your terminal after modifying PATH

Installation Fails with “Unable to find package”

Problem: dotnet tool install Morphir.CLI fails to find the package.

Solution:

  1. Check NuGet sources:

    dotnet nuget list source
    
  2. Add nuget.org if missing:

    dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
    
  3. Verify package name - ensure you’re using the correct package name

Validation Issues

“File does not exist” Error

Problem:

Error: File does not exist: morphir-ir.json

Solutions:

  1. Check file path:

    ls -l morphir-ir.json
    
  2. Use absolute path:

    morphir ir verify /full/path/to/morphir-ir.json
    
  3. Check current directory:

    pwd
    cd /path/to/ir/files
    morphir ir verify morphir-ir.json
    

Malformed JSON Errors

Problem:

Message: Malformed JSON: 'i' is an invalid start of a value. LineNumber: 6

Solutions:

  1. Validate JSON syntax with a JSON validator:

    cat morphir-ir.json | jq .
    
  2. Check for common issues:

    • Missing commas between array/object elements
    • Trailing commas (not allowed in JSON)
    • Unquoted keys or values
    • Invalid escape sequences
  3. Use a JSON formatter:

    cat morphir-ir.json | jq . > morphir-ir-formatted.json
    

Schema Version Detection Issues

Problem: Auto-detection selects the wrong schema version.

Solutions:

  1. Explicitly specify version:

    morphir ir verify --schema-version 3 morphir-ir.json
    
  2. Check formatVersion field (for v2/v3):

    cat morphir-ir.json | jq '.formatVersion'
    
  3. Verify IR structure:

    • v1: No formatVersion field
    • v2: "formatVersion": 2
    • v3: "formatVersion": 3

Validation Fails but IR Appears Correct

Problem: Validation reports errors, but you believe the IR is valid.

Investigation Steps:

  1. Review error messages carefully - they include Expected vs. Found values

  2. Check schema documentation - Schema Specifications

  3. Validate against different versions:

    morphir ir verify --schema-version 1 morphir-ir.json
    morphir ir verify --schema-version 2 morphir-ir.json
    morphir ir verify --schema-version 3 morphir-ir.json
    
  4. Compare with known-good IR:

    # Validate a reference file
    morphir ir verify reference-morphir-ir.json
    
  5. Check for common issues:

    • Incorrect tag capitalization (e.g., "Public" vs "public")
    • Missing required fields
    • Incorrect array structure
    • Type mismatches

Configuration

The shared libraries and morphir config get/list/show/path/explain provide multi-format resolution, bounded discovery and inspection. Configuration editing, classic project migration and bounded workspace migration use shared plan/apply services. Existing artifact/cache tooling retains TOML cache settings and Git-first discovery. See the guide for the library APIs and the command reference for read/write options and recovery semantics.

Wrong workspace or missing project

For a library host, inspect ProjectDiscoveryResult.Discovery, Selection, source reports, and diagnostics. Check the discovery boundary, the nearest declared workspace, member globs/exclusions, and the selected project. Use morphir config path --json to inspect this evidence. A project file does not hide a parent workspace. A Git root or an empty .morphir directory does not declare a workspace through the new API. If artifact-management commands use a different root, check their legacy Git-first policy before changing the declaration.

Ambiguous configuration files

Read every path named in the diagnostic. Keep at most one ordinary primary per format group across the root, hidden, and dot-config layouts. For example, morphir.yaml and morphir.yml conflict, as do morphir.toml and .morphir/morphir.toml. JSON, YAML, and TOML may coexist across groups. Supplemental morphir-workspace.json may coexist with the ordinary JSON primary.

A member is missing or cannot be selected

Inspect workspace.root, members, and exclude in the declared workspace. A missing literal member warns; an existing directory without configuration and an invalid member document remain identifiable as errors. Duplicate display names do not merge projects. Select the canonical member path to disambiguate. An explicit unknown name/path or an invalid default_member is an error, rather than a request to choose an arbitrary project.

A candidate was not read

A report of Missing means the optional path was absent. Failed retains a parse/access diagnostic and must not be treated as a miss. Discovery never follows discovered links, including a linked configuration directory. Check the host boundary and mount permissions. Inactive profiles, personal files under isolation, and machine sources under isolation are intentionally not read.

A machine file is ambiguous

Global candidates include the platform configuration directory and the alternate Morphir home. Two files in the same format group conflict even when they are in different directories. MORPHIR_HOME replaces only the home alternative. A relative XDG_CONFIG_HOME is ignored. Requesting non-isolated machine loading without MachineTree produces discovery.machine_host_required; pass a separately authorized tree together with the explicit machine locations. Neither API implies ambient machine-file access. Remove the unintended duplicate or use different format groups when deliberate layering is required.

A member output setting has no effect

workspace.out_dir belongs to the owning workspace. Member output overrides produce diagnostics and cannot relocate it. Read the separate owning workspace context rather than expecting the root’s workspace subtree to be inherited into the member document.

A setting still has its old value after an edit

For current hosts, inspect the resolved origins and source reports. Use morphir config explain <key> to identify contributing layers and the winning source. A successful scoped write can remain overridden by a member, profile, personal file, environment variable, or explicit CLI value. Check the target reported by the edit and distinguish scoped inspection from effective inspection.

A value reappears after unset

unset removes the entry only from its target TOML file. A lower-format file, inherited scope, or default can supply it again. Use get <key> --show-origin to find the newly effective source. Removing an override does not delete the inherited setting.

A profile did not load

Check the supplied ProfileSelection, MORPHIR_PROFILES, and base profile controls. The CLI equivalents are --profiles and --no-profiles. An empty profile environment selector disables profiles, and inactive files are not read. Named profiles are not activated by CI detection alone. Profile stems must be adjacent to an eligible primary layout; a dot-config primary uses config.profile.<name>.<ext>. Isolation retains active profiles while omitting machine and personal sources. For the existing loader’s separate CI behavior, inspect CiProfileMode and .morphir/morphir.ci.toml.

A scoped key is missing or a value is redacted

A scope reads only that source group. A workspace-scoped miss does not inherit a global value. Compare get <key> --workspace with get <key> --show-origin. Empty strings and arrays are present and exit 0; an absent queried key exits 1. Quote a segment containing a literal dot, for example 'codegen."custom.generator".enabled'.

[REDACTED] is safe presentation for sensitive names and opaque secret references. Inspection never executes a secret command. Parser diagnostic messages are sanitized because they can contain values; use diagnostic codes, positions and source paths to locate the problem. --json returns structured diagnostics while stderr retains safe human diagnostic text.

Microsoft configuration projection is rejected

The optional adapter rejects case-distinct paths, literal : separators and empty segments that cannot retain their canonical identity in .NET’s hierarchical view. Use the typed canonical result to inspect these names; registration must not silently overwrite them. See the adapter reference for projection diagnostics, generated array binding and published host qualification. Pinned dependency linker warnings remain visible; successful smoke execution is not a warning-free claim.

A configuration write fails

Check the reported target, permissions, parse/validation diagnostics, and whether another process changed the file. Explicit YAML/JSON file targets are readable but not writable through these commands; select a scope with a TOML target. For a concurrent-change conflict, reload and review the new contents before retrying. Failed validation or a write conflict must leave the file unchanged.

--json changes output only; it does not infer input types. Use --type bool for ui.color, --type int for an integer, or complete JSON arrays/objects with their explicit type. Nulls, duplicate object keys and out-of-range integers cannot be written as these TOML edit values. A missing unset exits 1 even when another source supplies the effective value.

At a workspace root, an automatic edit targets the workspace primary. --user follows the selected project, including default_member. Use the reported target before applying a preview. A successful root write can remain overridden by a selected member, profile, personal file or environment source; read the nested effective explanation and winning origins.

A stale plan includes more than a changed destination. New alternate primaries, changed lower-format sources, active-profile inputs and relevant directory inventories also invalidate it. Reload and review before retrying. Identical writes do not touch the timestamp.

Native trees do not follow links. On macOS /etc is a conventional symlink, so a default system edit can fail before writing. An explicitly authorized physical TOML path avoids that alias; do not relax a discovery boundary or follow an unrelated link to hide the error.

rolled-back is a failure with the original target restored and owned temporary data removed. recovery-required identifies retained stage, backup, journal or directory artifacts with their transaction owner. Preserve them and inspect the target/journal before restoring a matching backup. A cleanup failure can retain artifacts after the data outcome is already known. Do not assume an I/O failure means unchanged bytes, and do not delete recoverable data or another transaction’s artifacts. Native coordination is machine-local; a multi-machine store requires a host-supplied coordination protocol. Per-file atomic replacement does not imply multi-file group atomicity.

A classic project migration is blocked

Use morphir project migrate morphir.json --dry-run to inspect mappings, target conflicts, and verification results. Resolve conflicting TOML values deliberately. Nonempty legacy dependency or decoration structures need supported mappings; migration must not discard them merely because compatibility loading ignores them. An equivalence failure can also indicate a missing legacy default or changed relative-path meaning.

Validation and stale-plan failures leave both files unchanged. Verification excludes the classic source and ambient machine, personal, environment, CLI, and activated-profile overlays, so success demonstrates that the new file stands on its own in the declared base workspace context. Keeping morphir.json afterward is intentional for older consumers; it is not a failed migration. See the project command reference.

Workspace migration has a blocked member or needs recovery

Use morphir workspace migrate <directory> --dry-run to inspect the bounded inventory and all member plans. Exclusions and nested workspaces constrain what can be adopted. A conflict in one selected project blocks the batch before destination replacement.

If a commit fails after some replacements, inspect the reported restored/unresolved paths and retained recovery journal. Overlapping writes stay blocked until recovery is resolved. Do not delete recovery data to force another run. Successful application and complete rollback remove temporary transaction artifacts. See the workspace reference.

Performance Issues

Validation Takes Too Long

Problem: Validation of large IR files (>1MB) takes several seconds.

Current Limitations:

  • This is expected behavior for large files in Phase 1
  • Schema validation is inherently slower for complex, deeply-nested JSON

Workarounds:

  1. Use --quiet mode to reduce output overhead:

    morphir ir verify --quiet morphir-ir.json
    
  2. Validate in parallel (for multiple files):

    find . -name "*.json" | xargs -P 4 -I {} morphir ir verify --quiet {}
    
  3. Phase 2 improvements (coming soon):

    • Performance optimizations for batch processing
    • Caching and incremental validation

High Memory Usage

Problem: Validation consumes excessive memory for very large IR files.

Solutions:

  1. Check file size:

    ls -lh morphir-ir.json
    
  2. For files >10MB:

    • Consider splitting into smaller modules
    • Report the issue with file size details
  3. Increase available memory (if using Docker):

    docker run --memory 2g morphir-cli ir verify morphir-ir.json
    

CI/CD Integration Issues

CI Pipeline Doesn’t Fail on Invalid IR

Problem: Pipeline continues even when validation fails.

Solution: Check exit codes explicitly:

# GitHub Actions
- name: Validate IR
  run: |
    morphir ir verify morphir-ir.json
    if [ $? -ne 0 ]; then
      echo "Validation failed"
      exit 1
    fi

Or use set -e in bash scripts:

#!/bin/bash
set -e  # Exit on any error

morphir ir verify morphir-ir.json
echo "Validation succeeded!"

JSON Output Not Parsed Correctly

Problem: CI tools can’t parse JSON output.

Solutions:

  1. Verify JSON format:

    morphir ir verify --json morphir-ir.json | jq .
    
  2. Save to file:

    morphir ir verify --json morphir-ir.json > validation-result.json
    
  3. Check for extra output:

    • Use --quiet with --json to suppress non-JSON output
    • Some logging may appear on stderr

Permission Denied in Docker

Problem:

Permission denied: /app/morphir-ir.json

Solution: Fix file permissions or use volume mounts:

# In Dockerfile
RUN chmod +r /app/*.json

# Or when running
docker run -v $(pwd):/app:ro morphir-cli ir verify /app/morphir-ir.json

Error Message Reference

Common Validation Errors

Missing Required Field

Path: $.distribution
Message: Required properties ["formatVersion"] are not present
Expected: required property
Found: undefined (missing)

Fix: Add the missing field to your JSON:

{
  "formatVersion": 3,
  "distribution": [ ... ]
}

Type Mismatch

Path: $.modules[0].name
Message: Value is "string" but should be "array"
Expected: array
Found: string

Fix: Change the field type:

// Incorrect
"name": "MyModule"

// Correct
"name": ["my", "module"]

Invalid Tag Value

Path: $.modules[0].accessControl
Message: Value must be one of: ["Public", "Private"]
Expected: "Public" or "Private"
Found: "public"

Fix: Use correct capitalization:

// Incorrect
"accessControl": "public"

// Correct
"accessControl": "Public"

Array Structure Error

Path: $.distribution[2]
Message: Expected array to have exactly 4 elements
Expected: array of length 4
Found: array of length 3

Fix: Ensure array has the correct number of elements per schema.

Reporting Issues

If you encounter an issue not covered here:

  1. Check existing issues: GitHub Issues

  2. Gather information:

    • Morphir .NET CLI version: morphir --version
    • .NET SDK version: dotnet --version
    • Operating system and version
    • Complete error message
    • Minimal reproduction steps
  3. Create a new issue with:

    • Clear title describing the problem
    • Steps to reproduce
    • Expected vs. actual behavior
    • Environment information
    • Sample IR file (if possible) or minimal example

Management Commands (dist/tool/extension)

No Active Version Set

Problem: Running morphir dist which or morphir tool which <name> shows “No active version set”.

Solution:

  1. List installed versions:

    morphir dist list
    morphir tool list
    
  2. Set an active version:

    # Global
    morphir dist use <version>
    morphir tool use <name> <version>
    
    # Local (project-specific)
    morphir dist use <version> --local
    morphir tool use <name> <version> --local
    

Platform Not Supported

Problem: Installation fails with “Platform not supported” or downloaded artifact doesn’t work.

Solution:

  1. Check current platform:

    dotnet --info | grep RID
    
  2. Manually specify platform:

    morphir dist install <url> <version> --platform linux-x64
    
  3. Verify available platforms: Check the distribution documentation for supported platforms.

Local vs Global Confusion

Problem: Setting a version locally but it’s not being used.

Solution:

  • Remember precedence: Local (.morphir/) overrides global (~/.config/morphir/)
  • Check both scopes:
    morphir dist which          # Shows resolved version (local > global)
    morphir dist list           # Shows global installations
    morphir dist list --local   # Shows local installations
    
  • Clear local selection: Remove the active selection file in .morphir/dist/active or .morphir/tools/active-<name>

Installation Directory Not Created

Problem: Installation fails because parent directories don’t exist.

Solution:

The CLI should create directories automatically. If this fails:

  1. Manually create directories:

    # Global
    mkdir -p ~/.config/morphir/{dist,tools,extensions}
    
    # Local
    mkdir -p .morphir/{dist,tools,extensions}
    
  2. Check permissions:

    ls -ld ~/.config/morphir
    

Downloaded Artifact Is Corrupted

Problem: Installation completes but artifacts don’t work or are corrupted.

Solution:

  1. Check manifest for hash:

    cat ~/.config/morphir/dist/<platform>/<version>/manifest.json
    
  2. Verify download integrity (if SHA256 is in manifest):

    sha256sum ~/.config/morphir/dist/<platform>/<version>/bin/artifact
    
  3. Re-download:

    morphir dist remove <version>
    morphir dist install <url> <version>
    

Compile failures

Use --json and read failureKind and diagnostics, or --json-lines and read the last completed record. Exit code 1 is a source or model error; exit code 2 is an invocation, configuration, selection or I/O failure. See compile.

CodeCauseFix
compile.frontend_unavailableNo registered frontend reads the language. The Native AOT morphir has no built-in F# frontend.Use dotnet-morphir, which has a built-in F# frontend, or configure the F# MEP guest with --extension morphir-fsharp-frontend or [frontend.fsharp].extension.
extension.unavailableThe frontend extension did not start, or its session did not initialize.Check the --extension or [frontend.<language>].extension value: a command on PATH, a path, or a .dll. Read the guest’s stderr.
extension.not_admittedThe agreed MEP session does not admit the request, for example its IR version or a file extension.Request a version or documents that the message lists for the extension.
extension.tuple_unavailableThe host and the extension agreed no compileModel tuple (profile morphir-ir, codec morphir-json).Use an extension that serves JSON compile tuples for IR 4.0.0 or 3.0.0.
morphir.unrepresentableThe requested classic IR version cannot hold a value of the model: an integer outside the 64-bit range, or a character outside the Basic Multilingual Plane. Exit code 1.Request IR version 4.
compile.unknown_exposed_moduleproject.exposed_modules names a module that the sources do not define.Fix the module name, or remove it from exposed_modules.
msbuild.deadline_exceeded, msbuild.output_limit_exceededProject evaluation did not end before the deadline, or wrote more output than the limit.Check the project file for imports that run long or write much output.
extension.protocol_errorThe extension broke the MEP protocol, for example it wrote a log line to stdout.Fix or update the extension. Logs go to stderr.
extension.timeoutThe extension did not answer before the deadline.Check the guest’s stderr, and run the compile again.
compile.unsupported_ir_versionNo writer declares the requested IR version, for example 4.1.0-draft.1.Use a declared version: 1, 2, 3, 4 or 4.0.0.
compile.ir_version_not_servedThe selected frontend does not produce the requested IR version.Request a version that the message lists for the frontend.
compile.language_requiredNo language is selected, and the sources do not select one. A [frontend.<language>].project_file is read only for the selected language.Use --language, or set frontend.language.
compile.package_requiredNo package name.Use --package-name, or set project.name.
compile.unsupported_settingAn option or setting that this build does not implement, for example --types-only or ir.layout = "document-tree".Remove it. The message names the option or the key and its file.
compile.invalid_arguments naming --target or --generate-output--target without --generate-output, --target more than one time, or --generate-output or --option without --target.Give one --target with --generate-output <directory>. -o installs the IR; it is not the generation output.
incompatible-model after a successful compile with --targetThe backend of the target does not declare the IR version. The IR stays published.Use an IR version that the message lists for the backend, or run generate later with another target.
compile.generation_unavailable--target in a build that registers no generation backends.Use dotnet-morphir or morphir, which both register the F# and C# backends, or run generate on the published IR.
sources.conflicting_source_options--project-file and --input together.Use one of them.
sources.project_file_as_inputAn --input file is a project file.Use --project-file.
sources.unknown_project_fileNo project-file provider recognizes the file.Name a supported project file. See project-file sources.
sources.no_documentsThe source directory has no file with an extension that the frontend claims.Set project.source_directory, or use --input.
sources.unsupported_entry, sources.invalid_utf8A source, or a directory above it, is a link or a special file, or the source is not valid UTF-8.Name the target file, or save the file as UTF-8.
install.foreign_content-o names a directory that has a file that an earlier install of this task did not write. The directory does not change.Remove or move the file, or use another -o directory.
compile.record_missinggenerate without --input found no compile record, or the most recent compile failed.Run morphir compile first, or give --input.

If F# reports that a module or value is not defined, check the source order. A directory gives files in lexical order, and F# compiles files in order. Use --input files in dependency order, or a project file.

Generation failures

morphir generate requires explicit target and output paths. With --input, it does not read morphir.toml or morphir.yml. Without --input, it reads the IR of the most recent morphir compile of the project. Use exact target fsharp and Boolean option --option optimize=false to disable folding. Unknown options and quoted strings where Booleans are required return exit 2.

Use JSON/Ion bare IR, or an Ion tree rooted at manifest.ion. YAML models, JSON/YAML trees and MEP envelopes have explicit unsupported-format/profile diagnostics. A codec error identifies the input or tree member and logical location. A semantic refusal returns exit 1 and preserves previous output.

Keep input and output paths separate and remove links from output paths. Linked ancestors are also refused. On macOS, system temporary paths under /var can pass through an OS-provided link. Use the physical path or, in CI, a runner-owned temporary directory for model fixtures and generated output. Publication owns only paths listed in .morphir-generated.json. Staging failures preserve previous output; commit-time failures can leave mixed files. Fix the reported filesystem problem and rerun generation. In --json mode, parse stdout and collect logs from stderr separately.

See morphir generate for the complete command contract.

Getting Help

See Also

Project migration blocks or cannot write

Run morphir project migrate --dry-run --json and inspect ledger, conflicts, verification and diagnostics. Unsupported nonempty classic shapes and values TOML cannot represent must be reconciled explicitly. A conflicting target is never overwritten, even when an ambient override makes normal loading look equal. Use a supported same-project primary layout and retain one TOML primary.

A stale plan requires a fresh preview. Source, target, alternate-primary and base workspace evidence must still match before installation. Exit 2 also covers write failures. A rolled-back outcome restored the original target; a recovery-required outcome names owned journal, stage and backup artifacts. Preserve them and later user edits until manual recovery is resolved. Migration retains the original classic source and never edits workspace membership.

Workspace migration blocks or needs recovery

Run morphir workspace migrate <directory> --dry-run --json and inspect inventory, projects, diagnostics, and verified. The selected directory is the boundary even if it has no workspace. A child invocation does not adopt its parent or siblings. Excluded candidates, default skipped directories, links, and nested workspace boundaries appear in the inventory. Include/exclude controls are root relative and repeatable; existing workspace exclusions stay effective.

One unsupported classic field or conflicting TOML value blocks the entire batch. Resolve the reported project blocker and create a fresh preview. Migration retains classic JSON bytes, uses the single-project loss ledger, and excludes ambient personal/machine/profile/environment settings from equivalence checks. An adoption effect for shared output roots does not permit source-directory or setting drift. Generated artifacts remain where they are.

A conflict means input, target, listing, or topology evidence changed after planning. Replan after reviewing that change. Do not weaken filesystem boundary or link guards. On macOS use physical /private/tmp rather than its linked aliases.

recovery-required is unfinished work. Preserve all listed journals, stages, backups, and user-modified targets. Overlapping configuration edits, project migration, and workspace migration remain blocked until explicit recovery resolves the journal. uncertainDirectories identifies directories whose creation receipt was interrupted. Do not delete them even if they are empty. Explicit recovery can preserve reviewed uncertain directories and reports them in preservedDirectories. Inspect the shared transaction recovery report before choosing a recovery action. It identifies restored and unresolved paths; unresolved user changes are preserved. See workspace API and recovery for the supported API. There is no claim of cross-file crash atomicity, and silently deleting artifacts is not recovery.

Every JSON result, including parse or host-initialization failure, follows the workspace result schema. Exit 0 means applicable preview, success, or valid-workspace no-op. Exit 2 means blocked, invalid, failed, or recovery required. Diagnostics go to stderr and stdout contains one JSON document when --json is supplied.

7.14 - Compile

Compile source code to Morphir IR

Compile

compile reads source documents, compiles them with a frontend, writes the IR in the requested format version, and publishes it under the out root. With --target, it then generates code from the IR that it wrote, as generate does. The dotnet tool dotnet-morphir has a built-in F# frontend. The Native AOT executable morphir has no built-in frontend. A managed morphir built from src/Morphir.Managed has the same in-process F# frontend as dotnet-morphir, but it is not published yet.

Both executables can also run a frontend as a MEP 0.2.0-draft.1 extension: a guest process that the command starts and that compiles the documents. See Frontend extensions.

Synopsis

dotnet-morphir compile [--language <id>] [--input <file>... | --input <directory> | --project-file <file>]
  [--package-name <name>] [--ir-version <version>] [--project <path-or-name>] [--config <file>]
  [--extension <id-or-path>] [-o <directory>] [--out-dir <directory>]
  [--target <id> --generate-output <directory> [--option <key>=<value>]...] [--json | --json-lines]

Options

OptionMeaning
--language, -lSource language, for example fsharp. Overrides frontend.language.
--input, -iSource files in compilation order, or exactly one directory.
--project-fileOne build-tool project file that names the ordered sources. See project-file sources.
--package-namePackage name, for example acme/car-rental. Overrides project.name.
--ir-versionIR format version: 1, 2, 3, 4, or a SemVer version such as 4.0.0. Overrides ir.format_version.
--projectA project directory, a workspace member path, or a project name.
--configAn explicit configuration file.
--extensionA frontend extension: a command name, a path, or a .dll. Overrides [frontend.<language>].extension.
--output, -oInstall a copy of the IR in this directory after a successful compile. See Output.
--out-dirThe out root. Overrides MORPHIR_OUT_DIR and workspace.out_dir.
--targetA generation target, for example fsharp or csharp. Generate code from the compiled IR. See Compile and generate.
--generate-outputThe owned output directory of the generation. Necessary with --target.
--optionA backend option KEY=VALUE for --target, as in generate. You can give it more than once.
--jsonWrite one JSON report to stdout.
--json-linesWrite the compile events to stdout as JSON Lines, and end with the report.
--types-onlyNot implemented. The command refuses it with exit code 2.

Do not use --json and --json-lines together. Do not use --project-file and --input together. Give --target one time only, and always with --generate-output. --generate-output and --option need --target. The command refuses these errors with exit code 2 before it compiles.

Settings

Each setting comes from the first source that supplies it:

  1. a command-line option;
  2. the selected project configuration: project.name, project.source_directory, project.exposed_modules, ir.format_version, frontend.language, and the [frontend.<language>] keys project_file and extension;
  3. a default: IR version 4, source directory src, all public modules exposed.

--project and --config select the project through the usual project discovery. Outside a project, only the command line and the defaults apply. The report gives the origin of each setting in origins.

[project]
name = "acme/car-rental"
exposed_modules = ["Pricing"]

[ir]
format_version = 4            # or "4.0.0"; a declared pre-release such as "4.1.0-draft.1" is also valid

[frontend]
language = "fsharp"

[frontend.fsharp]
project_file = "src/Acme.CarRental.fsproj"   # the same key in every [frontend.<language>] table

A language id can contain a dot; then the id is one quoted TOML key, [frontend."acme.lang"]. Without the quotes, [frontend.acme.lang] is a table lang inside a table acme, which no language reads.

The upstream default of frontend.language is not a selection. If you do not set the language, the extensions of the documents select it.

IR versions

The command keeps the requested version exactly. It never reduces a version to its major number. 4 and 4.0.0 select the same writer, and the report records the version as you wrote it. A pre-release, for example 4.1.0-draft.1, matches only a writer that declares that exact version. This build declares 1, 2, 3 and 4. A version that no writer declares fails before compilation with compile.unsupported_ir_version and exit code 2.

Sources

The command selects sources in this order:

  1. --project-file: one project file;
  2. --input: files in the given order, or one directory;
  3. [frontend.<language>].project_file of the selected language;
  4. the files of project.source_directory.

A directory gives its files in lexical order of their relative paths, with / separators. It gives only files whose extension a registered frontend claims: .fs and .fsi for the in-process F# frontend, and .fs only for the F# MEP guest. The command does not search for project files, and it does not apply rules that belong to one language. F# compiles files in order, so use a project file or --input files when a file uses a module of another file.

The command reads each document as strict UTF-8. It refuses links and special files, and it names each refused document.

Frontends

The command selects a frontend by what each registered frontend advertises: the language (or the extensions of every document), the file extension of each document, the output IR version, and support for several documents. If no frontend matches, or more than one matches, the command fails before compilation and names the frontend and the capability.

  • dotnet-morphir registers the in-process F# frontend morphir-dotnet.fsharp.
  • The Native AOT morphir has no F# frontend. A compile of F# fails with compile.frontend_unavailable.
  • The managed morphir built from src/Morphir.Managed registers the same in-process F# frontend as dotnet-morphir. It is not published yet.
  • A configured extension replaces the registered frontends for the run. See Frontend extensions.

Frontend extensions

A frontend extension is a guest process that speaks the Morphir Extension Protocol (MEP) 0.2.0-draft.1 over stdio and serves morphir.frontend.compileModel. The F# frontend is available as the guest morphir-fsharp-frontend. Install it as a dotnet tool. It needs the .NET runtime:

dotnet tool install --global Morphir.Frontend.FSharp.Extension

Select the extension with --extension, or with [frontend.<language>].extension in the project configuration. The option overrides the key.

morphir compile --language fsharp --input src/Rates.fs --package-name acme/demo --extension morphir-fsharp-frontend
[frontend.fsharp]
extension = "morphir-fsharp-frontend"

The command reads the value in this way:

  • A name without a directory is searched only in the absolute directories of PATH, in order, and the command starts that absolute path. It is never taken from the invocation directory or from the directory of morphir itself. Starting a bare name directly would look there first: CreateProcess searches the application’s directory and the current directory before PATH on Windows, and .NET does the same on Unix. On Windows the name matches a .exe or .com file, in PATHEXT order, and batch files are not started. On Unix the file must be executable. A name that is not on PATH fails with extension.unavailable; to run a file in the current directory, give a path such as ./guest.
  • A relative path is relative to the invocation directory.
  • A path that ends in .dll runs on the dotnet host: DOTNET_HOST_PATH when it is set, else dotnet searched on PATH in the same way.

What configuration can run. [frontend.<language>].extension names a program that morphir compile starts with your user’s rights. The project configuration is the morphir.toml found from the working directory upward, so running morphir compile inside a checkout that you do not trust can start a program that the checkout chooses: a file inside the checkout, or any command on PATH. MORPHIR_ISOLATED=1 leaves out only the machine and personal configuration files, not the project’s own file, and MORPHIR_DISCOVERY_BOUNDARY only limits how far up the search goes. Read the extension keys of a project before you run morphir compile in it, or choose the file with --config.

The command starts the guest with explicit arguments and no shell, and passes the guest’s stderr to its own stderr. It agrees the session with initialize, and it admits only the model tuples that both sides support. The F# guest claims the language fsharp with the file extension .fs, several documents in one request, and these tuples: profile morphir-ir, codec morphir-json, envelope 0.1.0-draft.1, IR 4.0.0 and 3.0.0. IR versions 1 and 2 have no tuple. For them, the command compiles at 3.0.0 and writes the classic model at the requested version. Before it sends a request, the command checks that the session admits the language, the file extension of each document, the number of documents and the IR version. The guest does not claim .fsi signature files.

The command checks the model envelope of the result (base64, length and SHA-256) before it reads the IR. When the run ends, it sends shutdown and then exit. A guest that does not answer shutdown, or does not take exit, within the deadline (for example a guest that is still busy or no longer reads its input) has its whole process tree stopped. So a request that times out ends within about two deadlines: one for the request and one for the end of the session. The report names the frontend with its transport, for example org.finos.morphir.fsharp-frontend (mep). For the same documents, the guest gives the same IR and the same diagnostics as the in-process frontend. A .fsi signature file is the exception: no frontend in the session claims it, so the command refuses the compile before it sends a request. Without --language, the refusal is compile.language_required.

An extension failure is exit code 2 with one of these codes:

CodeMeaning
extension.unavailableThe guest did not start, the session did not initialize, or the guest gave an id reserved for built-ins (every id that starts with morphir.).
extension.not_admittedThe agreed session does not admit the request, for example its IR version.
extension.tuple_unavailableThe host and the guest agreed no compileModel tuple, so no request can be sent.
extension.protocol_errorThe guest broke the protocol, for example it wrote a log line to stdout.
extension.timeoutThe guest did not answer before the deadline.

Upstream qualification: the MEP guest kit of finos/morphir still needs a legacy 0.1 stage, so the guest is not qualified with it yet. The guest is checked against the pinned 0.2.0-draft.1 contract and the conformance corpus. See the F# frontend.

Output

A compile writes its IR to <out root>/[<member path>/]compile.dest/morphir-ir.json and its task record to compile.json beside that directory. These files use the out-directory contract of the Morphir ecosystem. The out root is the first of:

  1. --out-dir;
  2. MORPHIR_OUT_DIR, relative to the invocation directory;
  3. the workspace workspace.out_dir, relative to the workspace root;
  4. .morphir/out under the workspace root.

Without a workspace, the project root is the workspace root, or the invocation directory outside a project. The member path is the directory of a workspace member relative to the workspace root, for example packages/orders.

The record has schema 1, task compile, the language, the project file in inputs (relative to the workspace root), value, the ir descriptor (path, layout, format and version, for example v4), the installed ledger and completedAt. morphir generate without --input reads the IR through this descriptor. See Generate.

When a compile starts, it clears compile.dest and replaces an earlier record with a tombstone. A successful compile then writes the full record. A failed compile publishes no IR and leaves the tombstone, which keeps the installed ledger. The compile holds the exclusive lock compile.lock until its record and its install are written. Another run that waits for the lock writes one line to stderr:

waiting for another Morphir run to finish this task (<out root>/compile.lock)

-o <directory> installs a copy of the IR in the directory after a successful compile. It replaces only the files that an earlier install of this task wrote there, and it keeps all other files. If a file that it must write is already there and an earlier install did not write it, the command fails with install.foreign_content and exit code 2, and the directory does not change. The compile output in compile.dest stays published.

Compile and generate

compile writes its task output and record, and can install IR or generated files. It currently has no --dry-run option; --option format=false controls formatting of generated F# only.

With --target <id> --generate-output <directory>, a successful compile continues into generation in the same run. The command gives the exact IR bytes that it wrote to the model loader of generate --input, and then runs the generation stages of generate: backend selection, --option validation, generation and owned publication. The result is the same as these two commands:

dotnet-morphir compile ...
dotnet-morphir generate --input <out root>/compile.dest/morphir-ir.json --target <id> --output <directory>

Both give the same files with the same bytes, and both reports name the same backend, as long as the project configuration selects no backend extension. compile --target always runs the built-in backend of the target and does not read [codegen.<target>].extension; when that key is set, the command prints a note on stderr, and only morphir generate runs the extension. Classic IR versions (1, 2 and 3) give a classic model, and the backend uses its classic path. As in generate, the generation member says whether the built-in backend formatted the files (formatting: applied, off or unavailable), and a host that cannot run the formatter (the Native AOT morphir) writes a warning on stderr, never on stdout or in the JSON output. Pass --option format=false for the unformatted files without the warning.

  • Generation does not run when the compile fails.
  • When generation fails, the compiled IR stays published. The report has success: false, the output of the IR, the modules, and a generation member that names the target and, after backend selection, the backend. A backend that does not accept the IR version fails before generation, names the backend and the versions that it declares, and does not change the generation output.
  • A generation failure has the exit code of generate: 1 for a model that the backend cannot use, 2 for an invocation, option or publication failure, 130 for cancellation.
  • -o installs the IR. It is not the generation output. A run with more than one target is not part of this command.
$ dotnet-morphir compile --language fsharp --input src/Rates.fs src/Totals.fs --package-name acme/demo --target fsharp --generate-output generated --json
{
  "success": true,
  "exitCode": 0,
  "language": "fsharp",
  "frontend": "morphir-dotnet.fsharp (in-process)",
  "irVersion": "4",
  "sources": {
    "projectFile": null,
    "root": "src",
    "documents": [
      "Rates.fs",
      "Totals.fs"
    ]
  },
  "modules": [
    "Rates",
    "Totals"
  ],
  "output": "/work/.morphir/out/compile.dest/morphir-ir.json",
  "installed": [],
  "diagnostics": [],
  "failureKind": null,
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "default"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  },
  "generation": {
    "target": "fsharp",
    "backend": "morphir.fsharp",
    "output": "/work/generated",
    "sourceVersion": "4",
    "family": "V4",
    "distributionKind": "Library",
    "representation": "Json",
    "artifacts": [
      "Rates.fs",
      "Totals.fs"
    ],
    "written": [
      "Rates.fs",
      "Totals.fs"
    ],
    "unchanged": [],
    "removed": [],
    "formatting": "applied"
  }
}

In human mode, the summary on stderr also names the backend, the number of generated artifacts and the generation output. Stdout still holds only the path of the published IR.

Exit codes

CodeMeaning
0Success. Warnings do not change the exit code.
1The frontend, an IR builder or the writer reported a source or model error.
2An invocation, configuration, project-file, selection, extension or I/O failure.
130The command was cancelled before publication.

Human output

Without --json or --json-lines, the command writes each stage’s diagnostics to stderr when that stage completes, before later stages start. A project-file warning is therefore on stderr before a slow compile ends. The last stderr line is a summary that lists the same diagnostics. On success, stdout holds only the path of the published IR.

Pricing.fs:3:9: error fsharp.unsupported_mutable: Mutable bindings and fields are not supported; Morphir values are immutable. Use a new let binding, or a record update, instead of changing a value.
compile failed (source, exit code 1); 1 error(s): error fsharp.unsupported_mutable

JSON report

With --json, stdout holds exactly one JSON report. Logs go to stderr. The examples on this page use the sources of the MCK corpus cases frontend-fsharp-0116 (src/Rates.fs and src/Totals.fs) and frontend-fsharp-0001 (broken/Pricing.fs). output and sources.root show example paths.

$ dotnet-morphir compile --language fsharp --input src/Rates.fs src/Totals.fs --package-name acme/demo --json
{
  "success": true,
  "exitCode": 0,
  "language": "fsharp",
  "frontend": "morphir-dotnet.fsharp (in-process)",
  "irVersion": "4",
  "sources": {
    "projectFile": null,
    "root": "src",
    "documents": [
      "Rates.fs",
      "Totals.fs"
    ]
  },
  "modules": [
    "Rates",
    "Totals"
  ],
  "output": "/work/.morphir/out/compile.dest/morphir-ir.json",
  "installed": [],
  "diagnostics": [],
  "failureKind": null,
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "default"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  }
}

The report members are:

MemberMeaning
success, exitCodeThe outcome.
language, frontendThe language and the selected frontend, when known.
irVersionThe requested IR version, exactly as written.
sourcesThe project file (or null), the root, and the documents in compilation order. The root and the project file are relative to the invocation directory when they are in it, else absolute. Each document is relative to the root, with / separators.
modulesThe compiled modules, in compilation order.
outputThe published IR path, or null.
installedThe absolute files that the -o install of this run holds. Empty without -o.
diagnosticsEvery diagnostic of the run, ordered by stage, then document, then position.
failureKindsource, invocation, configuration, operational or cancelled, or null.
originsWhere each setting came from: cli with its flag, configuration with its file and key, or default.
generationOnly with --target: target, backend (null when selection failed), output, and the generate report members sourceVersion, family, distributionKind, representation, artifacts, written, unchanged and removed, and formatting when the backend reports it.

Diagnostics have the frontend (MEP) shape: severity, code, message, and optional location (a document uri and a zero-based range in UTF-16 code units), related and data. A location uri is relative to sources.root, as the documents are.

$ dotnet-morphir compile --language fsharp --input broken/Pricing.fs --package-name acme/demo --json
{
  "success": false,
  "exitCode": 1,
  "language": "fsharp",
  "frontend": "morphir-dotnet.fsharp (in-process)",
  "irVersion": "4",
  "sources": {
    "projectFile": null,
    "root": "broken",
    "documents": [
      "Pricing.fs"
    ]
  },
  "modules": [],
  "output": null,
  "installed": [],
  "diagnostics": [
    {
      "severity": "error",
      "code": "fsharp.unsupported_mutable",
      "message": "Mutable bindings and fields are not supported; Morphir values are immutable. Use a new let binding, or a record update, instead of changing a value.",
      "location": {
        "uri": "Pricing.fs",
        "range": {
          "start": {
            "line": 2,
            "character": 8
          },
          "end": {
            "line": 2,
            "character": 15
          }
        }
      },
      "data": {
        "construct": "mutable binding or field"
      }
    }
  ],
  "failureKind": "source",
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "default"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  }
}

A pre-release that no writer declares fails before compilation:

$ dotnet-morphir compile --language fsharp --input src/Rates.fs --package-name acme/demo --ir-version 4.1.0-draft.1 --json
{
  "success": false,
  "exitCode": 2,
  "language": "fsharp",
  "frontend": null,
  "irVersion": "4.1.0-draft.1",
  "sources": null,
  "modules": [],
  "output": null,
  "installed": [],
  "diagnostics": [
    {
      "severity": "error",
      "code": "compile.unsupported_ir_version",
      "message": "No IR writer declares format version 4.1.0-draft.1. Declared versions: 1, 2, 3, 4."
    }
  ],
  "failureKind": "configuration",
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "cli",
      "flag": "--ir-version"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  }
}

The Native AOT executable has no F# frontend:

$ morphir compile --language fsharp --input src/Rates.fs --package-name acme/demo --json
{
  "success": false,
  "exitCode": 2,
  "language": "fsharp",
  "frontend": null,
  "irVersion": "4",
  "sources": null,
  "modules": [],
  "output": null,
  "installed": [],
  "diagnostics": [
    {
      "severity": "error",
      "code": "compile.frontend_unavailable",
      "message": "No registered frontend reads the language 'fsharp'. This build registers no frontend. The self-contained and framework-dependent morphir and dotnet-morphir have a built-in F# frontend; this offering uses the F# frontend process delivery morphir-fsharp-frontend. Set [frontend.fsharp].extension in morphir.toml, or use --extension, to configure a frontend extension."
    }
  ],
  "failureKind": "configuration",
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "default"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  }
}

JSON Lines

With --json-lines, stdout holds only newline-delimited JSON. Each line is one compact UTF-8 JSON object with an event member. The command writes and flushes each line when its event occurs. Logs stay on stderr.

eventOther membersMeaning
stageStartedstageA stage starts.
diagnosticsstage, optional document, diagnosticsThe diagnostics of a stage for one document, or for no document.
stageCompletedstage, passedA stage completes.
completedreportThe last line, exactly once. report is the report that --json writes.

The stages are settings, sources, capture, frontend (which builds the requested IR family), write and publish, and with --target also generate (model loading, backend selection, option validation and generation) and generatePublish (owned publication of the generated files). A stage that does not run has no records. All diagnostics records together hold the same diagnostics as the report. The records keep emission order; the report keeps stage, document and position order. A parse error, the --json and --json-lines conflict, and cancellation also end with one completed record. Ignore event types and members that you do not know: new ones can be added without a new format version.

$ dotnet-morphir compile --language fsharp --input src/Rates.fs src/Totals.fs --package-name acme/demo --ir-version 3 --json-lines
{"event":"stageStarted","stage":"settings"}
{"event":"stageCompleted","stage":"settings","passed":true}
{"event":"stageStarted","stage":"sources"}
{"event":"stageCompleted","stage":"sources","passed":true}
{"event":"stageStarted","stage":"capture"}
{"event":"stageCompleted","stage":"capture","passed":true}
{"event":"stageStarted","stage":"frontend"}
{"event":"stageCompleted","stage":"frontend","passed":true}
{"event":"stageStarted","stage":"write"}
{"event":"stageCompleted","stage":"write","passed":true}
{"event":"stageStarted","stage":"publish"}
{"event":"stageCompleted","stage":"publish","passed":true}
{"event":"completed","report":{"success":true,"exitCode":0,"language":"fsharp","frontend":"morphir-dotnet.fsharp (in-process)","irVersion":"3","sources":{"projectFile":null,"root":"src","documents":["Rates.fs","Totals.fs"]},"modules":["Rates","Totals"],"output":"/work/.morphir/out/compile.dest/morphir-ir.json","installed":[],"diagnostics":[],"failureKind":null,"origins":{"language":{"kind":"cli","flag":"--language"},"packageName":{"kind":"cli","flag":"--package-name"},"irVersion":{"kind":"cli","flag":"--ir-version"},"input":{"kind":"cli","flag":"--input"}}}}

Difference from the Rust CLI

The Rust morphir CLI (finos/morphir) writes one compact report line for --json-lines. This CLI writes the event stream, one record per line, as the events occur. Until upstream aligns, the two CLIs give different --json-lines output. The last line of this CLI is a completed record that holds the full report, so a consumer that reads only the last line and its report member works with this CLI. An upstream issue proposes this record format as the shared meaning.

Unimplemented options

These fail with exit code 2 and compile.unsupported_setting, which names the option or the key and its file: --types-only, ir.format = "yaml" or "ion", and ir.layout = "document-tree".

See also

7.15 - Extension installation status

What the extension installer currently implements and what remains

Extension installation status

extension install supports an explicit local-developer release declaration and archive from a registered repository name, with --no-probe consent and a mutation-free --dry-run. The obsolete positional URL/version form is a usage error. Selection and removal commands remain stubs. The management reference gives the command syntax. This does not grant publisher authentication or claim published-release acquisition.

Historical implementation evidence

The following snapshots retain their original source identities and limitations; they do not describe the current command surface.

The current library increment on ad24ce68 can install a release-bound local ZIP into its canonical package tree, register it through the published catalog/lock transaction, and capture immutable selected records with an owned package lease. Local no-probe installation is explicitly named InstallLocalUncheckedAsync and stays labelled local-developer/unchecked. Supported interrupted journals can be recovered with explicit consent; old formats are not converted.

Unpublished snapshots remain expected-current inventory. They cannot authorize a new acquisition after current metadata changes. Retained-generation acquisition requires a supported published binding, which is not implemented by this increment. No new CLI command or generation metadata format is provided here.

The review correction refuses a foreign process-platform activation before it returns ownership or registers local metadata. Windows x64 emulation remains supported. Malformed compressed ZIP content returns a typed archive refusal. Contended state/repository operations return asynchronously and cancellation waits for native lock ownership to settle rather than abandoning a waiter.

Fresh Windows installer assembly runs before and after formatting each pass 285 executed tests, with seven Unix controls ignored and no failures or errors. These include transaction interruptions, journal replacement, empty ZIP directories, exact current-export refusal, native cancellation and real cross-process leases. The existing C# consumer also passes its 27-line JIT run; it does not cover the new package APIs or establish AOT support. Tar packages, repair, full corpus checks, commands and the actual process-provider journey remain unfinished. Independent security/interop re-review, package C# consumer coverage, Unix runtime and Native AOT qualification are still gates. Parent composition contracts are merged at d9f309a1; their runtime binding remains unimplemented here. The earlier 194-test snapshot and evidence below keep their original source identities. Independent review requested security corrections to installation I/O. The fixes now pass 131 executed Windows tests; three Unix-only interleavings were explicitly ignored. Independent security/interop re-review of b6936ba5 found no concrete remaining finding under the approved cooperating Home boundary. Unix runtime evidence and Native AOT remain qualification gates. These results do not qualify a released installer.

Subsequent macOS CI at 4c908047 ran 180 tests, with 177 passing, two failing and one error in file permissions/reopening and temporary-directory disposal. A Darwin ARM64 native-call correction and permission regressions are now under review. Focused Windows verification passes Store 15/15 and RunCopies 5/5, with three Unix-only tests ignored and Unix permission assertions unexecuted. The correction subsequently passed the installer assembly on hosted macOS and Ubuntu at exact head 749dd2fe: 182/182 each, with zero ignored, failed or errored. That closes the ABI platform-evidence gap. Native AOT, acquired artifacts and the full installation lifecycle remain unqualified.

Private streaming stages are the next test-first library draft. After separate missing-API and setup diagnostics, a fresh build succeeded and 16 tests ran. All 16 errored at the declaration stub’s intentional refusal; zero passed or were ignored. No staging mechanism executed, and no behavior or public API approval is established by that earlier stub run. The Begin/Write/Seal implementation now passes 25/25 focused Windows tests that execute byte ownership, integrity, interrupted-write and concurrency controls, including cancellation while waiting for creation admission. Affected Store 15/15 and RunCopies 5/5 pass, with three Unix-only cases ignored. The public C# JIT probe also passes its original 15 output cases plus nine staging cases, including ownership, nullable cleanup, refusal and cancellation controls. Hosted macOS and Ubuntu at 98df4035 then each reported 207 executed, 182 passed and all 25 staging tests errored, with zero ignored or failed. Begin incorrectly declared a raw Unix file handle asynchronous. A source correction preserves the open file and Windows OVERLAPPED behavior while using the Unix handle’s actual mode. Focused Windows verification passes 25 staging, 15 Store and five RunCopies tests before and after formatting, with three Unix-only tests ignored each time. These Windows results leave the hosted Unix failure open; Unix staging GREEN and AOT evidence remain pending. The reviewed hashing dependency was merged normally; publication, host mapping, manager integration and later safe orphan collection remain unfinished. This adds no working installation command.

The next increment drafts tests for publishing sealed private bytes through the existing single-file store. Declaration-only refusal stubs now compile; a focused Windows run executed 17 cases, all errored, with four Unix-only cases ignored. No publication mechanism ran in that stub check. A source implementation now connects sealed ownership to the same store publisher/verifier as the path API. Two fresh focused Windows builds succeed, each with four existing dependency warnings and zero errors. Before and after pinned formatting, the permitted lists each pass 17 sealed-publication, 25 staging, 15 Store and five RunCopies tests. Seven Unix-only cases are ignored in each run. No working command, current Unix qualification or AOT qualification follows from these checks. Independent review found no finding within this sealed-publication scope. The public C# JIT consumer subsequently passes with 27 output lines, preserving its original 24 and adding checks for published bytes/digest/length, nullable cleanup and one-time sealed ownership. Its fresh build has four existing dependency warnings and zero errors; the run has empty stderr. This remains Windows development evidence. Required final verification includes the actual published bytes and Unix mode, including a macOS clone. Orphan collection, host mapping and acquisition lifecycle integration remain unfinished. Prior Windows results do not qualify the newly merged source or replace hosted Unix evidence. The coordinator separately verified the earlier 56665f6c installer suite on hosted macOS and Ubuntu, 207/207 each with zero ignored, failed or errored. That evidence belongs to that exact earlier source.

The installation library can verify and publish a staged single file to a content-addressed store, validate ZIP and tar-gzip archives without extracting them, and clean stale temporary directories owned by this host. It checks a single file against its recorded SHA-256 while copying and rehashes a stored file before reuse. A mismatched stored object is refused and left untouched. Archive inspection checks names, entry types and size, count and expansion limits before anything can be published.

Catalog and lock integration, package extraction, install-time capability probes, activation and installation commands remain unfinished. Shared package formats will use the parent contract and corpus published in finos/morphir#1156 at 0439c4c0. Portable pins and generation-overlay document formats still wait on the separate parent toolchain contract.

The first internal shared-reader increment passes 19 focused Windows tests using data pinned to that parent commit, including six release/schema cases and two I-JSON cases. It is not connected to installation commands. The remaining parent case operations and full claim/package validation are still being implemented.

The internal claims reader additionally passes 27 focused Windows tests for current-schema claims, recursive metadata/comparison and host requirements. It shares embedded parsing with the descriptor reader and uses the existing version comparator library. This reads claims supplied in metadata; install-time probing and publisher authentication remain separate, unfinished integrations.

The maintainer approved project/workspace pins resolved from local inventory and MORPHIR_EXTENSION_OVERLAY above project pins. Explicit frontend extension_id and extensions table id fields will select installed identities without changing existing path-string meaning. CMD-1 keeps remove as an uninstall alias and --platform only on extension list. These commands and selection integrations are not implemented by this approval.

Checksums establish integrity, not who published an extension. A capability check is a separate fact too. Naming a local source or explicitly installing staged files is developer consent, and the result stays labelled local-developer. Automatic unsigned staged installation requires an explicitly configured auto setting; inheriting historical default auto from an omitted setting is not consent. The schema default remains unchanged, and staging integration/tests are still pending. No library operation in this increment authenticates a publisher.

Morphir Home is user-owned. Supported writers must honor stable locks and detected tampering must be refused. This model does not isolate an arbitrary malicious process running as the same user; a separate broker or sandbox is not promised.

Ordinary compilation and generation must not acquire extensions from the network. Staged-file policy will remain explicit. Later commands that change installation state will support --dry-run before writing anything.

The technical library notes describe the current APIs, store layout, archive bounds and platform verification limits. Native AOT qualification and Linux/macOS filesystem runs remain pending.

7.16 - Generate

Generate target artifacts from a Morphir model

Generate

Generate target files from a decoded Morphir library. Targets fsharp and csharp support classic and v4 models. Target and output are explicit. The input is an explicit --input, or else the IR of the most recent morphir compile of the project.

Synopsis

morphir generate [--input <file-or-directory>] --target <fsharp|csharp> --output <directory>
  [--input-format auto|json|ion] [--option KEY=VALUE ...] [--json]
  [--extension <path-or-command>] [--project <path-or-name>] [--config <file>] [--out-dir <directory>]

Options

OptionMeaning
--inputJSON/Ion model file or Ion tree directory. Without it, the IR of the most recent compile
--targetRequired exact registered target ID, fsharp or csharp
--outputRequired output directory with owned generated files
--input-formatauto by default; json or ion constrains the reader
--optionRepeatable backend option, described below
--jsonWrite one structured report to stdout
--extensionA backend extension: a command name, a path, or a .dll. Overrides [codegen.<target>].extension. See Backend extensions.
--projectThe project directory, workspace member path or project name: its compile record without --input, and its configuration
--configAn explicit configuration file of the project
--out-dirWithout --input: the out root. Overrides MORPHIR_OUT_DIR and workspace.out_dir

Paths resolve against the invocation directory. With --input, no project configuration is required. Without --extension, generate reads the project configuration only for [codegen.<target>].extension. With --project or --config, a configuration error fails the command with exit code 2, as it does for compile. With neither, a discovered configuration that does not load is a warning on stderr, and the built-in backend runs. Repeated option keys use the last value. Values parse as JSON first, so optimize=false is Boolean while optimize='"false"' is a string and is refused by this backend.

generate writes generated files and an ownership manifest. It currently has no --dry-run option; --option format=false controls formatting only.

F# accepts the Boolean options optimize and format, each true by default:

  • optimize runs exact constant folding.
  • format formats the generated source with Fantomas when the host can run it. format=false writes the generator’s own text instead: LF line endings only and exactly one final LF, UTF-8 without a byte order mark, the same bytes on every operating system, under JIT and Native AOT, and in every culture. Use it for output that must not depend on the host, such as the Morphir compatibility check.

format is a backend option, like optimize; it is not --input-format, which says how to read the input. The external F# backend morphir-fsharp-backend accepts the same two options, and they reach it through the options member of morphir.backend.generateModel.

The default formatted text follows the platform’s line endings (CRLF on Windows, LF elsewhere). The Native AOT morphir cannot run Fantomas, so under the default it writes the generator’s own text, says so on stderr, and reports formatting: unavailable. The managed hosts (dotnet-morphir and the managed morphir) format.

C# accepts numeric JSON language-version=14 or language-version=15, default 14, and optional string namespace=Pricing.Generated. Plain namespace text becomes a JSON string through the shared option parser. Numeric strings, preview, latest, unsupported numbers, unknown keys and wrong JSON kinds are invocation errors.

C# namespaces are dotted literal identifiers in Unicode Normalization Form C. An identifier can start with a letter or _ and continue with letters, decimal digits, connector punctuation, combining marks or formatting characters. Reserved keywords require @, for example @class.Models. Literal BMP Unicode is supported, such as Café.模型; source \u/\U escapes and surrogate characters are refused. The core validates this same vocabulary for direct callers. An override changes the input package’s emitted namespace and its qualified references. Relative source paths retain the original package identity.

Inputs and target profile

JSON accepts classic integer versions 1, 2 and 3, their supported semantic release declarations, and v4. Ion text, binary and trees support releases 3.0.0, 3.1.0 and 4.0.0. Automatic detection recognizes the binary marker even without an Ion suffix. An Ion tree has manifest.ion at its root.

The F# target supports Library implementations at exact versions 1, 2, 3, 3.0.0, 3.1.0, 4 and 4.0.0. It emits types, values and mapped SDK operations. Specs/Application, document literals, open records, holes, incomplete/native/ external bodies, nonempty constraints and unknown required semantics are refused. Integers use Int32, floats finite doubles, decimals exact .NET decimal values with retained scale up to 28, and characters one BMP scalar. Unsupported or lossy representations fail before publication.

YAML model files, JSON/YAML trees and MEP envelopes are unsupported. MEP admission is a separate change. Generation produces source files and their ownership manifest. It does not create an F# project or compile and execute the output.

The C# target declares the same exact Library versions. It uses the immutable Morphir.SDK.CSharp runtime and the admitted C# generation contract. Its own semantics and located refusals remain those of the reusable C# library. Generation needs no compiler installation. A separate consumer chooses the matching numeric compiler profile and references the runtime package. The production Roslyn adapter remains deferred.

Backend extensions

A backend extension is a guest process that speaks the Morphir Extension Protocol (MEP) 0.2.0-draft.1 over stdio and serves morphir.backend.generateModel. Select one with --extension, or with [codegen.<target>].extension in the project configuration. The option overrides the key. compile --target does not read the key: it runs the built-in backend and prints a note on stderr when the key is set. Without either, the built-in backend of the target runs, as before. The F# generator is available as the guest morphir-fsharp-backend; see F# generation.

morphir generate --input model.json --target fsharp --output gen --extension morphir-fsharp-backend
[codegen.fsharp]
extension = "morphir-fsharp-backend"

The table is named by the target id. A target id can contain a dot; then the id is one quoted TOML key, [codegen."acme.codegen"]. Without the quotes, [codegen.acme.codegen] is a table codegen inside a table acme, which no target reads.

The command reads the value as compile reads a frontend extension:

  • A name without a directory is searched only in the absolute directories of PATH, in order, and the command starts that absolute path. It is never taken from the invocation directory or from the directory of morphir itself. Starting a bare name directly would look there first: CreateProcess searches the application’s directory and the current directory before PATH on Windows, and .NET does the same on Unix. On Windows the name matches a .exe or .com file, in PATHEXT order, and batch files are not started. On Unix the file must be executable. A name that is not on PATH fails with extension.unavailable; to run a file in the current directory, give a path such as ./guest.
  • A relative path is relative to the invocation directory.
  • A path that ends in .dll runs on the dotnet host: DOTNET_HOST_PATH when it is set, else dotnet searched on PATH in the same way.

What configuration can run. [codegen.<target>].extension names a program that morphir generate starts with your user’s rights. The project configuration is the morphir.toml found from the working directory upward, so running morphir generate inside a checkout that you do not trust can start a program that the checkout chooses: a file inside the checkout, or any command on PATH. MORPHIR_ISOLATED=1 leaves out only the machine and personal configuration files, not the project’s own file, and MORPHIR_DISCOVERY_BOUNDARY only limits how far up the search goes. Read the extension keys of a project before you run morphir generate in it, or choose the file with --config.

generate loads the model first, then starts the guest with explicit arguments and no shell, and passes the guest’s stderr to its own stderr. It agrees the session with initialize and offers JSON IR in the tuples profile morphir-ir, codec morphir-json, envelope 0.1.0-draft.1, IR 4.0.0 and 3.0.0. A v4 model is sent in the 4.0.0 tuple and a classic version 3 model in the 3.0.0 tuple, as the JSON bytes of the input file. Other versions and Ion input are not sent to an extension. The command checks every artifact path that the guest returns, keeps the guest’s artifact order, and publishes the files as for a built-in backend. When the run ends, it sends shutdown and then exit. A guest that does not answer shutdown, or does not take exit, within the deadline (for example a guest that is still busy or no longer reads its input) has its whole process tree stopped. So a request that times out ends within about two deadlines: one for the request and one for the end of the session.

The report names the backend that ran by its own extension id, and adds backendTransport (mep) and backendVersion, the version that the extension gives. A report of a built-in backend has neither member.

An extension failure is exit code 2 with one of these codes, or exit code 1 when the guest rejects the model with error diagnostics:

CodeMeaning
extension.unavailableThe command is not on PATH, the guest did not start, the session did not initialize, the guest does not claim the target, or the guest gave an id reserved for built-ins: every id that starts with morphir. (such as morphir.fsharp), and the id of any registered built-in backend.
extension.tuple_unavailableThe host and the guest agreed no generateModel tuple, or the model needs a tuple that the session did not agree. The message names the tuples.
extension.not_admittedThe guest refused the request, for example an option, or the host did not send the model because it is over the limits that the session agreed (by default 45 MiB decoded, 60 MiB of base64, depth 4096, 50,000,000 nodes).
extension.unsafe_artifactThe guest returned an artifact path that is not a plain relative path; a segment that ends in a period or a space, or is a reserved Windows device name (CON, NUL, COM1 and the like, also with an extension); two paths that differ only in case; or a path that is also the directory of another path. Nothing is published.
extension.protocol_errorThe guest broke the protocol, for example a result that is not a generate result, more than 10,000 artifacts in one result, malformed describe claims, or a -32800 answer that the host did not ask for.
extension.timeoutThe guest did not answer before the deadline.
invalid-extensionThe value of --extension or of the key is empty.

Selection by an installed extension id is not available: the value is a command, a path or a .dll, as for compile.

Input from a prior compile

Without --input, generate reads the IR of the most recent successful morphir compile of the project. It finds the project and the out root in the same way as compile: --project and --config select the project, and the out root is --out-dir, else MORPHIR_OUT_DIR, else the workspace workspace.out_dir, else .morphir/out under the workspace root. generate then reads the compile record compile.json and loads the IR that its ir descriptor names. The report gives that IR file as input and the recorded IR version as sourceVersion. An explicit --input never reads a record.

If there is no compile record, or the most recent compile failed and left a tombstone, generate fails with compile.record_missing and exit code 2, tells you to run morphir compile first, and publishes nothing. If the IR file does not declare the version that the record names, it fails with compile.record_version_mismatch. An --input-format that is not the format of the compiled IR fails with generate.input_format_conflict.

generate holds the shared lock compile.lock from when it reads the record until it has loaded the IR. While a compile of the same project runs, generate writes one waiting line to stderr, waits, and then reads the IR of that compile. A compile that starts while generate loads the IR waits for generate.

With this morphir.toml in the project directory, and the sources src/Rates.fs and src/Totals.fs of the MCK corpus case frontend-fsharp-0116:

[project]
name = "acme/demo"

[frontend]
language = "fsharp"

compile the project, then generate F# from the compiled IR. Run both commands in the project directory. The compile writes the path of the published IR to stdout, and generate writes the generated files:

$ dotnet-morphir compile
/work/demo/.morphir/out/compile.dest/morphir-ir.json
$ dotnet-morphir generate --target fsharp --output gen
Rates.fs
Totals.fs

Examples

Run these from a repository checkout, using its shared fixtures:

morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v3.json --target fsharp --output artifacts/generated-fsharp
morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target fsharp --output artifacts/generated-fsharp --option optimize=false --json
morphir generate --input tests/Fixtures/MorphirIon/pairs/v3-daily-rate.ion --target fsharp --output artifacts/generated-fsharp --input-format ion --json
morphir generate --input tests/Fixtures/MorphirIon/trees/v3-daily-rate --target fsharp --output artifacts/generated-fsharp --json

For C#:

morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v3.json --target csharp --output artifacts/generated-csharp --json
morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target csharp --output artifacts/generated-csharp --option language-version=14 --option namespace=Pricing.Generated --json
morphir generate --input tests/Fixtures/MorphirIon/pairs/v3-daily-rate.ion --target csharp --output artifacts/generated-csharp --input-format ion --json
morphir generate --input tests/Fixtures/MorphirIon/trees/v3-daily-rate --target csharp --output artifacts/generated-csharp --json

--option language-version=15 --option language-version=14 selects 14. Selecting 15 explicitly generates the C# 15 profile independently of the SDK installed on the generation machine. Compile it separately using the matching .NET 11 compiler. See the executable C# walkthrough.

For CI, retain the JSON report and propagate a failure:

morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target fsharp --output artifacts/generated-fsharp --json |
    Set-Content artifacts/generation-report.json
if ($LASTEXITCODE -ne 0) { throw "Morphir generation failed" }

Output formats and exit codes

Human output lists relative artifact paths on stdout and diagnostics on stderr. JSON mode writes one document containing success, exitCode, target/backend, input/output, source version/family/kind/representation, ordered artifacts, written, unchanged, removed, diagnostics and failureKind, for a selected backend extension backendTransport and backendVersion, and formatting when the backend reports it: applied (formatted), off (format=false) or unavailable (the host cannot run the formatter). Only a built-in backend reports it. An external backend (backendTransport mep) has no formatting member, even with format=false: a MEP generate result does not say whether the guest formatted, and the host does not report what it did not observe. Logs and warnings use stderr, never stdout. Failed reports contain no successful artifact list. Parsed JSON-mode invocation errors also produce a report.

Structured diagnostics remain fields in that JSON report. JSON failures need not duplicate them as human-readable stderr text. Human-mode diagnostics and any emitted log text use stderr. C# reports identify backend morphir.csharp.

ExitMeaning
0Generation and publication succeeded
1Model or generation semantics are unsupported or invalid
2Invalid invocation, unavailable provider or operational failure
130Cancelled before publication

For a single-module fixture, artifacts contains ["Inventory.fs"]. Its order is the F# compilation order, including on repeat runs when files are unchanged. For the C# daily-rate fixture it contains ["p_Car.Rental/m_InventoryModule.g.cs"]. Both direct generation and the CLI use those same package-derived paths and source bytes.

Publication and ownership

Generated files are published through the shared tree publisher. The output directory contains .morphir-generated.json, an ownership manifest listing artifact paths in declared compilation order. Repeat generation leaves byte-identical files and the manifest untouched. Reports distinguish written, unchanged and removed paths.

Files listed by the previous manifest are removed when no longer generated. Files outside that ownership list remain untouched. Keep the manifest alongside the output so later runs can identify stale owned files.

Input and output paths must be separate. Rooted artifact paths, traversal, case collisions and links are refused before publication. Generation errors preserve previous output. A failure while staging also preserves previous files and the ownership manifest.

Publication does not provide directory-wide rollback. A failure while committing staged files can leave a mix of old and new files. Such failures return an operational error; inspect the output and rerun generation after fixing the reported filesystem problem.

Common errors

Missing settings require an explicit --target and --output. Without --input, generate needs a prior morphir compile of the project. An unknown target lists registered target IDs. Invalid F# options require the Boolean optimize or format, for example optimize=false or format=false. Codec diagnostics identify malformed models and tree members. Limit diagnostics describe the exceeded byte, node, depth, entry or path budget. See CLI troubleshooting.

7.17 - Validate

Check a Morphir model with the built-in analyzers

Validate

morphir validate reads a Morphir model and runs the enabled analyzers on it. An analyzer reads the model and reports diagnostics. It does not change the model or write files. The command reads the model in the same way as morphir generate.

Synopsis

morphir validate --input <file-or-directory> [--input-format auto|json|ion] [--json]

Options

OptionMeaning
--inputRequired. A JSON or Ion model file, or an Ion tree directory. The path resolves against the invocation directory.
--input-formatauto (the default), json or ion. json and ion select the reader.
--jsonWrite one JSON report to stdout.

The input rules are those of morphir generate: the same readers, the same detection, the same limits and the same refusals (for example YAML input, or a MEP model envelope instead of bare IR).

Analyzers

Both hosts, morphir and dotnet-morphir, include the same built-in analyzers. morphir extension list shows them with the type validator.

IdReadsReports
morphir.type-validatorClassic models, format versions 1, 2 and 3Definite type mismatches in value definitions, with the code type-mismatch

The type validator reports a mismatch only when it is sure: for example, a String literal where the definition declares Int. Types that it does not know stay silent. An IR node has no source position, so the diagnostic has no location, and the message starts with the module and value name.

The run rule:

  • Every enabled analyzer whose IR versions include the model’s format version runs. The diagnostics merge into one result.
  • The result is in a stable order: by extension id, then in the order in which each analyzer reported.
  • When no enabled analyzer reads the model, the result has one info diagnostic with the code no-analyzer that names the family and the format version. The command does not convert the model to another family. Today this is the result for every V4 model.
  • When an analyzer fails, the result names it with the status failed and still holds the diagnostics of the other analyzers.

Output

Without --json, the command writes a table of the diagnostics to stdout, then one line for each analyzer that did not run, then a summary line:

$ morphir validate --input model.json
╭──────────┬───────────────┬──────────┬───────────────────────────────────────────────────────────────────────────────────────────────┬────────────────────────╮
│ SEVERITY │ CODE          │ LOCATION │ MESSAGE                                                                                       │ EXTENSION              │
├──────────┼───────────────┼──────────┼───────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┤
│ error    │ type-mismatch │          │ Inventory.dailyRate: Value has type Morphir.SDK.String.String, declared                       │ morphir.type-validator │
│          │               │          │ Morphir.SDK.Basics.Int.                                                                       │                        │
╰──────────┴───────────────┴──────────┴───────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────────╯
1 error, 0 warnings, 0 info, 0 hints. Analyzers: 1 ran, 0 not run, 0 failed.

In a terminal the table has styled borders and headers and fits the terminal width. When stdout is redirected, or NO_COLOR is set, the table has no ANSI codes. A location is written as uri:line:character, one-based. When there is no diagnostic, only the summary line is written.

When the command stops before any analyzer runs (an invalid option, or an input that cannot be read), it writes the diagnostics to stderr and nothing to stdout.

With --json, the command writes one report:

$ morphir validate --input model.json --json
{
  "host": "morphir",
  "success": false,
  "exitCode": 1,
  "input": "/work/model.json",
  "family": "Classic",
  "formatVersion": "3",
  "analyzers": [
    {
      "id": "morphir.type-validator",
      "status": "ran"
    }
  ],
  "diagnostics": [
    {
      "severity": "error",
      "code": "type-mismatch",
      "message": "Inventory.dailyRate: Value has type Morphir.SDK.String.String, declared Morphir.SDK.Basics.Int.",
      "extension": "morphir.type-validator"
    }
  ]
}
FieldMeaning
hostThe host that wrote the report: morphir or dotnet-morphir. The two hosts write the same report apart from this field.
successtrue when the exit code is 0.
exitCodeThe exit code of the command.
inputThe full path of the input.
family, formatVersionThe model’s family (Classic or V4) and its declared format version. Left out when the input could not be read.
analyzersEach analyzer, ordered by id, with its status: ran, failed (with a detail) or not-run (with a reason: turned-off or unsupported-model).
diagnosticsThe diagnostics, in the MEP Diagnostic shape: severity (error, warning, info or hint), code, message, location (uri and a zero-based range), related and data, plus extension, the id of the analyzer that reported it. Absent members are left out. A diagnostic of the command itself has no extension.

Logs go to stderr only, so morphir validate --input model.json --json | jq .exitCode works.

The command does not change the model file or the working directory.

Exit codes

CodeMeaning
0No diagnostic has the severity error. Warnings and information are in the result.
1At least one diagnostic has the severity error.
2An invalid invocation, an input or configuration that cannot be read, or an analyzer that failed.
130The command was cancelled.

Configuration

The command reads the layered configuration of the invocation directory, with the same discovery as morphir config. It reads only extensions."<id>".enabled. The value false turns off the analyzer with that id. Extension settings lists every key of the table:

[extensions."morphir.type-validator"]
enabled = false

With this morphir.toml in the project directory, the same command gives:

╭──────────┬─────────────┬──────────┬───────────────────────────────────────────────────────────────────┬───────────╮
│ SEVERITY │ CODE        │ LOCATION │ MESSAGE                                                           │ EXTENSION │
├──────────┼─────────────┼──────────┼───────────────────────────────────────────────────────────────────┼───────────┤
│ info     │ no-analyzer │          │ No enabled analyzer reads the Classic family at format version 3. │           │
╰──────────┴─────────────┴──────────┴───────────────────────────────────────────────────────────────────┴───────────╯
morphir.type-validator: not run (turned-off)
0 errors, 0 warnings, 1 info, 0 hints. Analyzers: 0 ran, 1 not run, 0 failed.

A configuration with an error stops the command with exit code 2 before any analyzer runs.

validate and ir verify

  • morphir ir verify checks that a JSON file has the shape of the Morphir IR JSON schema. Use it to check a file that another tool wrote.
  • morphir validate decodes the model and checks what it means, for example the types of its values. Use it before you generate code from a model.

A model that passes ir verify can still have a type mismatch. A model that validate cannot read gives exit code 2.

External analyzers

Only built-in analyzers run now. External analyzers, delivered as WASM extensions, come in step 2a of the Morphir extension model PRD. They will report the same diagnostic shape and follow the same run rule.

8 - Plugin Development Guide

Develop typed plugins for the Morphir IR pipeline

Plugin Development Guide

A plugin is one typed step in a pipeline. It receives the model, can change it or report on it, and passes it on. This guide covers the plugin contract, how plugins fail, the plugins in Morphir.IR.Pipeline.Plugins, and how to test your own.

The pipeline README covers parsers, compilers, file content and publishing.

The plugin contract

type Plugin<'T> =
    { Name: string
      Configure: PropertyBag -> PropertyBag
      Transform: 'T -> VFile -> ('T option * VFile) }

'T is the model type, so a plugin receives the parsed model with no cast, and a plugin of the wrong type does not compile into a processor.

  • Name identifies the plugin.
  • Configure adds settings to the processor’s data when the plugin is added. Most plugins use id, which is what Plugin.create does.
  • Transform receives the model and the file. It returns the model to pass on, or None to reject it, along with the updated file.

The smallest plugin:

open Morphir.IR.Pipeline

let double = Plugin.create "double" (fun (value: int) file -> (Some (value * 2), file))

Reporting diagnostics

Plugins report through the file. A message can name a logical location, a rule and its source, so people and tools can act on it:

let rejectNegative =
    Plugin.create "reject-negative" (fun (value: int) file ->
        if value < 0 then
            let message =
                { VMessage.create Error "The value must not be negative." with
                    Location = Some "Rules.total"
                    RuleId = Some "negative-value"
                    Source = Some "reject-negative"
                }

            Some value, file |> VFile.report message
        else
            Some value, file)

VFile.info, VFile.warn, VFile.error and VFile.fail add simpler messages. When the run ends, the file’s messages move to the output tree.

How a plugin stops a run

The pluginLater pluginsCompilers
Adds an Info or Warning messageRunRun
Adds an Error message and returns the modelRunDo not run
Adds a Fatal messageDo not runDo not run
Returns NoneDo not runDo not run

Prefer an Error and the model when later plugins can still find useful problems, so one run reports as much as it can. Return None when later plugins would be working on something broken.

A run with an Error produces no files, and publishing refuses a tree that reports errors, so a failed run cannot replace good output.

Sharing data between plugins

Declare a typed property once and share it. Values go in and come out without casts:

let rowCount = Property.create<int> "my-library.row-count"

let countRows =
    Plugin.create "count" (fun (rows: string list) file ->
        Some rows, file |> VFile.set rowCount rows.Length)

let reportRows =
    Plugin.create "report" (fun (rows: string list) file ->
        match VFile.tryGet rowCount file with
        | Some count -> Some rows, file |> VFile.info (sprintf "%d rows" count)
        | None -> Some rows, file |> VFile.warn "Rows were not counted." None)

Use a dotted name that starts with your library. Two properties refer to the same value only when both their name and value type match.

Data on the file reaches the output tree’s metadata when the run ends, so callers read it with VFileTree.tryGet.

Running on part of a model

Plugin.focus runs a plugin on one part of a larger model. It takes a function that reads the part, and one that puts an updated part back:

type Model = { Version: int; Rows: string list }

let sortRows = Plugin.create "sort" (fun (rows: string list) file -> (Some (List.sort rows), file))

let sortModelRows: Plugin<Model> =
    Plugin.focus (fun model -> model.Rows) (fun rows model -> { model with Rows = rows }) sortRows

If the inner plugin returns None, the whole model is rejected.

The built-in plugins

Morphir.IR.Pipeline.Plugins works on classic IR. Each plugin is generic over the package’s attribute types, so it runs on any PackageDefinition<'ta, 'va>.

Optimizer

Optimizer.create takes Optimizer.Options:

OptionsRewrites
Optimizer.foldingOnlyFolds SDK operations on literals. Never changes which models a later stage accepts.
Optimizer.allAlso rewrites boolean short-circuits, if on a literal and identity lambdas. These can remove code a later stage would have rejected.

Folding only happens when the result matches what generated code computes at run time. Optimizer.tryFold returns None when:

  • the function is not an SDK function, so a model’s own add is never folded
  • a whole number, whether an input or the result, does not fit the int32 that Morphir Int maps to
  • a division is by zero, or a float result is not finite
  • the literal types differ
  • the operation has no generated mapping, such as Basics.and or String.append

Each change adds an Info message naming the value.

TypeValidator

TypeValidator.create () reports an error with the rule type-mismatch only when both sides are SDK primitive types that cannot match, such as a string literal under a declared Int. Anything it cannot infer counts as unknown and is silent, including model types and aliases, so it never rejects a valid model. It always keeps the model, so later plugins can report more. It does not report unbound variables.

PrettyPrinter

PrettyPrinter.create () formats every value in a package into the typed property PrettyPrinter.output, leaving the model unchanged. PrettyPrinter.createWithConfig takes a configuration built from defaultConfig, withColors, withIndent and withoutTypes.

open Morphir.IR.Classic
open Morphir.IR.Pipeline.Plugins

let inspect (package: Package.PackageDefinition<unit, unit>) =
    let parser: Parser<Package.PackageDefinition<unit, unit>> = fun _ -> Ok package

    let processor = pipeline {
        parse parser
        uses (TypeValidator.create ())
        uses (Optimizer.create Optimizer.foldingOnly)
        uses (PrettyPrinter.create ())
    }

    let tree = MorphirProcessor.processFile VFile.empty processor
    VFileTree.tryGet PrettyPrinter.output tree

Writing a safe transformation

A transformation must not change what a program does. For each rewrite, ask what the generated code would have computed, including at the edges:

// Safe only inside the range of the type Morphir Int maps to
let foldAdd (a: int64) (b: int64) =
    let inRange (value: int64) = value >= int64 System.Int32.MinValue && value <= int64 System.Int32.MaxValue

    if inRange a && inRange b && inRange (a + b) then Some (a + b) else None

Check the inputs as well as the result. A literal out of range is rejected by the backend today, so folding it into something in range would make a rejected model start generating.

Match functions by their fully qualified name. A model can define its own function called add.

Rewrite bottom-up, so (1 + 2) + 3 collapses completely, and keep the attributes of the expression you replace, because later stages can read them.

Testing plugins

Call Transform directly to test a plugin by itself, and assert on the model and the messages:

open Expecto

[<Tests>]
let tests =
    testList "double" [
        test "should double the value" {
            let result, file = double.Transform 21 VFile.empty

            Expect.equal result (Some 42) "should double"
            Expect.isEmpty file.Messages "should report nothing"
        }
    ]

Then test it in a processor, since that covers its effect on later stages:

let runWith plugin =
    let parser: Parser<int> = fun _ -> Ok -1

    MorphirProcessor.empty
    |> MorphirProcessor.parse parser
    |> MorphirProcessor.plugin plugin
    |> MorphirProcessor.processFile VFile.empty

test "should report a negative value on the tree" {
    Expect.isTrue (VFileTree.hasErrors (runWith rejectNegative)) "the error should reach the tree"
}

For a transformation that changes generated code, compile and run the output with the plugin on and off and compare the results. optimizationEquivalenceTests in tests/Morphir.CodeGeneration.FSharp.Tests does this for the optimizer.

A complete example

Transpiler in Morphir.CodeGeneration.FSharp is a production pipeline. It parses a classic model, validates scopes and types, folds constants and emits one F# file per module, and the Myriad plugin and build task both call it.

Next steps

9 - Shared configuration editing

Plan one scoped TOML edit, inspect its effective result, and explicitly apply it

Shared configuration editing

Configuration edits belong to Morphir.Configuration and Morphir.Configuration.IO. The CLI supplies arguments and renders results. Embedded hosts can supply discovery, typed documents, environment and filesystem evidence, then preview without a writer.

EditRequest distinguishes set and unset. Strings are the default. Boolean, integer, number, array and object inputs use explicit typing. Composite inputs are JSON values; JSON output does not imply JSON input. Arrays and objects replace the complete entry in the target. TOML has no null value, so nulls in edit inputs are rejected, including nested array/object nulls. Ordinary configuration readers retain their existing null behavior.

Editing.selectTarget selects one destination from captured ownership and inventory. Editing.plan produces immutable before/after values, a candidate document, validation, observations and a redacted effective explanation. It performs no I/O. Canonical plan values are for application and must be treated as sensitive data; use the redacted explanation and shared redactor for presentation.

Default edits target the selected project. At a declared workspace root they target the workspace primary, including a virtual workspace. Explicit selectors choose workspace, one project, adjacent personal overrides, global, system or one TOML file. Missing or ambiguous contexts fail. File scopes exclude synthetic default layers.

Personal target selection considers every eligible layout belonging to its owner, not only the directory containing the preferred primary. An existing personal TOML remains the target across mixed primary layouts; multiple personal TOML candidates remain ambiguous. If only a lower-format personal source exists, its corresponding TOML stem is used.

An explicit personal file belonging to another declared member keeps that member’s context for validation and effective-value reporting. It remains a member-personal source under isolation, so a workspace output setting in that file cannot relocate the owning workspace.

A scope with only JSON/YAML creates the corresponding TOML primary. Lower-format bytes stay unchanged. Unset removes an authored TOML entry and adds no tombstone. A lower value can reappear with its original origin. A higher environment, personal or profile source can keep winning after a successful file write. The effective explanation makes that visible.

Presentation and input compatibility

The editor patches TOML tokens and reparses through the existing pinned reader. It verifies the intended typed change and all unrelated values. It preserves unrelated comments, whitespace, ordering, line endings and text. It does not rewrite a document through a serializer. Explicit JSON/YAML targets fail. Syntax acceptance remains the canonical reader’s contract; the editor adds no parser package or TOML version upgrade.

Application and recovery

A preview is read only. Application checks identities of the observed source, destination, candidate inventory and relevant discovery inputs, including absent alternate primaries. A stale observation returns a conflict before replacement. Replan after inspecting the other writer’s change. An identical edit does not rewrite the destination.

The transaction service coordinates overlapping cooperating writers, stages on the destination filesystem and installs each file atomically. It does not offer multi-file group atomicity. It retains backups and journal evidence until success or proven rollback. A fully rolled-back fault reports failure and cleans owned temporary data. An incomplete recovery returns a typed recovery-required result with retained artifact paths; preserve those files and reconcile the affected destinations before retrying.

External programs that do not participate in coordination can still race with the final filesystem operation. Preconditions detect observed changes; they are not a filesystem-wide lock on arbitrary editors. Generated-output publication and its manifests are separate and retain their existing guarantees.

Embedded consumption

The pure API accepts a complete captured EditTargetContext, ResolutionInput and observation list. It does not access the environment or filesystem:

let request = {
    Operation = EditOperation.Set("false", EditValueType.Boolean)
    Key = "ui.color"
}

let preview =
    Editing.selectTarget [EditScope.Project] targetContext
    |> Result.bind (fun target -> Editing.plan target request resolutionInput observations)

match preview with
| Ok plan -> plan.Effective // redacted effective value, contributions and origins
| Error failure -> failwith failure.Code // typed exit category and safe diagnostics

For an IO-backed host, supply authorized trees and storage explicitly:

var tree = new FileSystemProjectTree(projectBoundary);
var storage = new NativeConfigurationTransactionHost();
var editor = new ConfigurationEditService(tree, storage);
var request = new EditRequest(
    EditOperation.NewSet("false", EditValueType.Boolean), "ui.color");
var preview = editor.Plan(invocationDirectory, request);

if (preview.Failure is { } failure)
    return failure.ExitCode;

// Display a redacted preview. No writer has been acquired yet.
var explanation = preview.Plan!.Effective;
// Apply only when the host decides to commit this exact preview.
var result = editor.Apply(preview);

An in-memory host can implement IConfigurationTransactionHost and use InMemoryProjectTree. Observe must identify the same bytes returned by ReadFile; file identities are uppercase SHA-256 hex. Directory identities represent the captured relevant inventory. The host must honor the writer coordination and per-file atomic installation contract. The transaction service copies observations and candidate bytes into immutable collections.

ConfigurationEditPreview.Failure carries a code, exit category and safe diagnostics. A successful preview contains both the shared EditPlan and immutable ConfigurationTransactionPlan. ConfigurationTransactionResult.Status distinguishes Written, Unchanged, Conflict, IoFailure, RolledBack and RecoveryRequired. Artifacts identifies retained paths, their kind and transaction owner. A retained recovery artifact is not permission to delete it.

Native stages are on the destination filesystem. Native coordination is deliberately coarse, serializing configuration writers through an OS mutex, including writers targeting different scopes. It leaves no lock files in the project. Embedded hosts may provide finer coordination if every overlapping writer participates.

10 - Microsoft configuration integration

Opt into standard .NET configuration while retaining typed Morphir values and metadata

Microsoft configuration integration

Morphir.Configuration.Microsoft adds a read-only provider to an IConfigurationBuilder. It projects one already resolved Morphir result. It has no CLI dependency and does not alter logging or remove existing host configuration providers.

The canonical DocumentResolution remains authoritative. Its typed values, origins, source reports, active profiles, workspace context and diagnostics stay available through GetMorphirConfiguration(). Do not reconstruct that document from flattened strings.

Register an explicit result

using Microsoft.Extensions.Configuration;
using Morphir.Configuration.IO;
using Morphir.Configuration.Microsoft;

var tree = new FileSystemProjectTree("/repo");
var discovered = ProjectSystem.Discover(tree, "/repo/app");
var project = discovered.Selection.Projects.Single();
var canonical = ProjectSystem.Load(tree, discovered.Discovery, project,
    new ProjectLoadOptions { Isolated = true });

var builder = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string?>
    {
        ["host:unrelated"] = "retained"
    });
builder.AddMorphirConfiguration(canonical);
var configuration = builder.Build();
var target = configuration["codegen:targets:0"];
var typed = configuration.GetMorphirConfiguration();

Supply an absolute authorized boundary and start directory. Check discovery and selection diagnostics before choosing a project. Omit the project argument when loading the workspace root. See the discovery guide for member selection and explicit machine-file authorization.

An overload performs the same single resolution before registration:

builder.AddMorphirConfiguration(tree, discovered.Discovery, project,
    new ProjectLoadOptions { Isolated = true });

The overload uses the supplied bounded tree and loading options. It does not infer ambient machine access. Neither overload registers raw JSON, YAML or TOML providers. Morphir resolves ownership, profiles, complete array replacement and shape replacement before flattening. A shorter higher-layer array therefore has no stale indexes from lower Morphir files.

Bind options without reflection

For source-generated binding, enable the generator in the consuming C# project and reference Microsoft.Extensions.Configuration.Binder 10.0.14:

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>

Use a concrete options type at the binding call site:

var options = configuration.GetSection("codegen").Get<CodegenOptions>();

public sealed class CodegenOptions
{
    public string[]? Targets { get; set; }
    public string[]? Missing { get; set; }
}

With targets = [], Targets is present and empty. An absent missing setting leaves Missing null. The provider emits the empty-string marker supported by .NET 10’s generated array binder. Nonempty arrays use zero-based indexed keys. Bind fresh options with nullable array properties as above. The adapter does not promise replacement behavior for arbitrary existing option instances or reflection-based binding of F# unions.

Explicit binding can instead read canonical values directly. This retains type and presence distinctions independently of the standard configuration view:

using Microsoft.FSharp.Collections;
using Morphir.Configuration;

var value = ConfigTree.tryFind(ListModule.OfSeq(new[] { "codegen", "targets" }),
    typed.Effective);
var explicitOptions = new CodegenOptions
{
    Targets = value?.Value is ConfigValue.Array array
        ? array.Item.Select(item => ((ConfigValue.String)item).Item).ToArray()
        : null
};

The application owns its options type, validates required fields, and can register explicitly constructed options in its own dependency container. The adapter performs no reflection-based discovery of consumer types.

Projection and failure contracts

APIResult
MorphirConfigurationProjection.Create(DocumentResolution)Canonical result, immutable projected Values, typed Diagnostics, and IsSuccess
AddMorphirConfiguration(DocumentResolution)Adds one provider on success; throws MorphirConfigurationException on failure before changing the builder
GetMorphirConfiguration()Returns the canonical result of the most recently registered Morphir provider; fails if none was registered

MorphirProjectionDiagnostic contains Code, Message, and Paths, with canonical quoted key paths. Failed projections have an empty Values map while retaining the original Canonical result. The exception also exposes Canonical and Diagnostics.

  • config.microsoft.collision rejects case-distinct paths such as custom.Foo and custom.foo, including collisions at parent objects. Literal colon segments such as custom."a:b" are rejected even without a competing path, since standard .NET readers would reinterpret them as hierarchy. Empty key segments are rejected because they alias .NET section boundaries.
  • config.microsoft.invalid_resolution refuses a result with error diagnostics.
  • config.microsoft.invalid_root refuses a non-object document root.

The flattened view formats scalar values with invariant culture. For example, integer 4 and string "4" both project to "4"; their canonical types remain distinct. Arrays and nested objects flatten after Morphir resolution. Empty objects and null array elements retain their canonical meaning even where a standard string consumer cannot infer it.

The standard projection uses the shared inspection redaction policy. Sensitive values and opaque secret references become [REDACTED]. Canonical access is unredacted host data and must be handled accordingly. Registration never resolves an environment/file/keyring reference or executes a secret command. The provider rejects Set; it does not implement mutation, reload or watching. Unrelated providers remain in the builder. Canonical arrays, scalar values and empty objects own their replacement subtrees: earlier host providers cannot revive discarded array indexes, old descendants or redacted container values. Standard .NET precedence still lets a provider registered later override the Morphir view.

Published non-CLI example

The runnable smoke host uses both binding paths, typed metadata, native and mounted trees, replacement and empty arrays, collision diagnostics, shared redaction, and generated JSON serialization with reflection disabled. Managed, trimmed and Native AOT runs each pass 43 checks, including earlier-host-provider array and redacted-value boundaries. Its README gives managed, trimmed and Native AOT commands and records dependency-warning limitations. This qualifies the documented concrete binding path, not arbitrary consumer types.

11 - Extension settings

The [extensions.] table: which keys exist, which ones a command reads, and how a WASM module is checked

Extension settings

Each extension has a table named after it, at any configuration level:

[extensions.python]
path = "/opt/morphir/extensions/morphir-python-binding.wasm"
abi = "extism-kernel-v1"
sha256 = "10acafa0c79d7f0d3a1c43f2f3b3c4b2f8c25c20c1a2a5f8e3c2b1a0f9e8d7c6"
KeyTypeRead today by
pathstringThe WASM module used by experimental Python compile/generate commands.
abi"extism-kernel-v1" or "morphir-bytes-v1"The WASM module loader.
sha25664 lowercase hexadecimal digitsThe WASM module loader.
urlstringNothing.
commandstringNothing.
argsarray of stringsNothing.
enabledbooleanmorphir validate. false turns off the analyzer with that id.
configtableNothing.

Configuration validation checks the type of every key in the table, and the form of abi and sha256. Other keys in the table are kept and not checked. abi and sha256 are local additions to the shared Morphir configuration schema, proposed upstream. tests/Fixtures/Configuration/PROVENANCE.md records them.

A WASM module

path names a WASM module. The host reads the file and decides from its bytes what it is. The file name and its extension play no part: a module called binding.txt loads, and a text file called binding.wasm does not.

Experimental morphir compile --language python and morphir generate --target python use the configured Python module. See the pinned Python walkthrough. morphir extension list does not list configured modules yet (#565).

Give path as an absolute path or relative to the configuration file that supplies it. The settings resolver resolves it before passing the module to the loader.

WASM extensions says how the host runs a module once it loads: one instance for each session, and the memory, instruction and time bounds of each call. A session can hold twice the memory ceiling, 512 MiB by default, because the guest and the Extism kernel each have a memory of their own.

Declare the ABI

abi names the profile you expect the module to have. The host still classifies the module from its imports. The declaration only confirms the result and never changes it. If the two differ, the module is refused.

This host runs extism-kernel-v1 modules only. morphir-bytes-v1 is a valid value, because it is a profile of the contract, but the host refuses a module of that profile whether you declare it or not.

Declare the digest

sha256 pins the exact bytes. The host computes the SHA-256 of the file before it looks at the content, and refuses any other bytes. A module replaced on disk is then refused, even when the new file is a valid module. Without sha256, the host reads, hashes and classifies the file again on every load, so a replaced module runs as its new bytes.

Write the digest in lowercase, with no sha256: prefix:

sha256sum morphir-python-binding.wasm
(Get-FileHash morphir-python-binding.wasm -Algorithm SHA256).Hash.ToLowerInvariant()

Checks and failures

The host checks a configured module in this order. The first check that fails decides the diagnostic, so a declared digest that differs is reported even for a file that is not a module.

CheckDiagnosticWhat to do
A file exists at pathThe WASM module of extension 'python' cannot be loaded: there is no file at '<path>'.Correct path.
The file can be readThe WASM module of extension 'python' at '<path>' cannot be read: <reason>path names a directory, a file you may not read, or an invalid path. Name the module file.
The bytes match sha256... is refused: its SHA-256 is <actual>, but the configuration declares sha256 = "<declared>".If you replaced the module on purpose, update sha256 to the new digest. If not, restore the module you pinned.
The bytes are a WASM moduleThe file '<path>' of extension 'python' is not a WASM module (SHA-256 <digest>): <engine report>path names another file, or the download is damaged.
The engine supports the module’s featuresextension.wasm_feature_unsupportedThe module uses a shared, 64-bit or second memory, or a proposal the engine does not enable. Use a build of the extension without it.
The module conforms to its profile and this host runs itextension.abi_unsupportedThe message names the import outside the profile, the missing handle export, or the profile this host does not run. A morphir-bytes-v1 module needs an extism-kernel-v1 release of the extension.
A declared abi equals the classified profileextension.abi_mismatchThe message names both profiles. Correct abi, or remove it.

Each message after the read check states the module’s SHA-256. Only the last three checks have a code: the WASM execution profile contract defines those refusal codes, and it defines none for the file checks.

A bad value for abi or sha256 fails configuration validation with config.validation, naming the key, for example extensions.python.sha256. The message is Value is not one of the permitted choices. for abi and String does not match the required pattern. for sha256.

Guest output

The loader writes nothing to stdout or stderr. It returns every diagnostic as a value to the command that called it. Guest log messages and guest stdout and stderr will go to the host’s stderr, bounded per session, and never to stdout. The WASI sandbox and the worker mode that capture that output are not built yet.

12 - WASM extensions

How the host runs a WASM extension: one guest instance per session, and the memory, instruction and time bounds of each call

WASM extensions

The host runs WASM extensions of the extism-kernel-v1 profile: an Extism guest that speaks the Morphir Extension Protocol (MEP) 0.2.0-draft.1. Extension settings says how to name a module and how the host checks it before it runs.

Experimental morphir compile and morphir generate run a configured Python module through the same runtime as the internal morphir extension worker. The bounds below apply to each session.

One instance for each session

Each session gets its own instance of the guest and its own instance of the Extism kernel, in a store of its own. Two sessions of the same module share no memory and no state. A guest that fails ends its own session only.

Bounds of a session

BoundDefaultWhen a guest reaches it
Memory ceiling256 MiB for each linear memorySee Memory.
Instruction budget5,000,000,000 units for each callThe call stops with extension.budget_exhausted.
Call deadline30 seconds for each callThe call stops, and the session ends with outcome deadline.

The worker takes --memory-pages, --instructions and --call-ms to set other values for one run. A page is 64 KiB, so the default ceiling is 4096 pages.

Memory

An Extism session has two linear memories: the guest’s own memory, and the memory of the Extism kernel, which holds the guest’s input, its output and what it allocates through the kernel. The ceiling applies to each of them separately. A session’s linear memory can therefore reach twice the ceiling: 512 MiB with the default of 256 MiB. The code the engine compiles from the module, and the host’s own memory, come on top of that. Plan for twice the ceiling for each WASM session that can run at the same time.

A memory cannot grow past its ceiling. The engine refuses the growth, and the guest decides what happens next:

  • The guest stops with a trap. If the guest’s memory or the kernel’s memory is at its ceiling when the call ends, the session fails with extension.memory_limit.
  • The guest reports the failure itself, for example with an Extism error or a non-zero result. The session fails with that failure, extension.incomplete_session, even when a memory is at its ceiling.
  • The guest handles the refusal and answers. The call succeeds.

Every failure diagnostic states both memory sizes and the ceiling, so you can see which memory was full:

extension.memory_limit: the guest failed at the memory ceiling (the guest trapped, with the guest's memory at its ceiling): <what the engine reported>; guest memory 256 MiB (268435456 bytes), kernel memory 1 MiB (1048576 bytes), ceiling 256 MiB (268435456 bytes)

A module whose memory starts above the ceiling does not start. It fails with extension.memory_limit before any guest code runs.

In worker mode the same failure is a fault event on stderr with kind memory-limit, and the worker exits with status 1.

If an extension needs more memory, raise the ceiling for it. Each memory can then grow to the new ceiling, so the session can reach twice the new value.

13 - Classic project migration APIs

Pure planning, independent equivalence and guarded application without a CLI dependency

Classic project migration APIs

Morphir.Configuration.ProjectMigration owns the pure conversion policy. Morphir.Configuration.IO.ProjectMigrationService supplies bounded discovery, source-preserving TOML rendering and guarded application. Neither library depends on System.CommandLine or the CLI host. The command reference covers options, before/after files and exit codes.

A directory input must belong to the selected project. An unlisted project inside a workspace is refused rather than falling back to a sibling or a containing root project. Invoking from the workspace root may still select its unambiguous member; invoking within a declared member selects that member.

Read-only planning

var tree = new InMemoryProjectTree("/repo", new Dictionary<string, string>
{
    ["/repo/morphir.json"] =
        "{name:'Orders',sourceDirectory:'elm-src',exposedModules:['Order']}"
});
var service = new ProjectMigrationService(tree);
var preview = service.Plan("/repo", outputPath: ".config/morphir/config.toml");
var safePlan = preview.Presentation;
// safePlan includes redacted candidate, edits, raw-field ledger and conflicts.
// preview.Verification includes redacted snapshots, differences and origin changes.

A parsed-only tree needs no write adapter. It receives the complete logical plan and a TOML representability check, without writer coordination or artifacts. CanApply means the logical conversion is verified and applicable; that host still needs a storage adapter to create a fresh plan and apply it. The canonical Plan.Candidate and Plan.Edits support programmatic application and may contain secrets. Present Presentation, Verification and Diagnostics to users. Secret references stay opaque and are never evaluated.

Pure conversion and verification

ProjectMigration.plan accepts an original SourceInput, a TOML target identity and the target’s raw typed value. planWithContext also accepts declared base resolution context. Raw classic input is authoritative for loss accounting, including compatibility-reader fields that ordinary loading ignores.

The ledger accounts for each original value as mapped, equivalently retained or blocking. Derived entries record materialized defaults. Canonical dependencies objects and supported extra settings survive; unsupported nonempty classic shapes block. Null, out-of-range TOML integers and invalid Unicode in values or any key segment block even if another layer would hide them. Classic/canonical collisions in one file block regardless of equality. Equal target entries remain unchanged; missing leaves become edits; differing values produce redacted source/target conflicts.

ProjectMigration.verify accepts the actual parsed candidate TOML raw tree. It independently resolves before and after snapshots, excluding the converted classic source and old target from the candidate. Both sides retain only root and selected-member primary sources. Machine, selected-file, personal, environment, explicit CLI and activated-profile contributions are removed. Declared profile controls are compared separately using their raw merge semantics; activation stays disabled. Missing or changed declarations fail verification. Declared base workspace context remains, with its output directory recomputed from root base sources rather than a previously resolved personal override.

The comparison checks typed values before redaction, including complete arrays, map shape, signed zero and secret references. It also checks resolved project source-directory and workspace output paths. The project location remains the anchor for every primary layout. Expected origin changes appear separately. An unrelated default ir.format_version is never written into the candidate. For a member, base workspace pipeline values that override legacy low-priority checkpoint defaults are preserved when those defaults are materialized.

Explicit application

var root = ProjectPath.normalize(Path.GetFullPath("orders"));
var service = new ProjectMigrationService(
    new FileSystemProjectTree(root), new NativeConfigurationTransactionHost());
var preview = service.Plan(root);
if (preview.CanApply)
{
    var result = service.Apply(preview);
    // Inspect Status, Code and Artifacts. RecoveryRequired needs explicit recovery.
}

Only the creating service can apply its immutable, verified, adapter-backed preview. Exact source and target bytes plus discovery observations, missing alternate primaries and relevant workspace evidence become transaction preconditions. Stale source, target or workspace evidence blocks installation. The converted source is never a write target and remains byte-identical.

Application reuses the editor’s transaction boundary. It stages bytes beside the target, coordinates cooperating writers, rechecks preconditions and replaces one file atomically. An identical repeat preserves bytes and timestamp. A write failure reports rollback or retained recovery artifacts with transaction ownership. Preserve journals, stages and backups when recovery is incomplete; do not remove a subsequently edited target to force a rollback. No multi-file crash atomicity or distributed-writer guarantee is claimed.

Native trees reject linked ancestors. On macOS use physical /private/tmp paths for temporary roots. A host authorizes each tree boundary explicitly; migration never borrows access from machine roots or modifies workspace membership.

14 - Configuration transactions and recovery

Configuration transactions and recovery

WorkspaceMigrationService, ProjectMigrationService and ConfigurationEditService apply through the shared ConfigurationTransaction boundary. Configuration tools coordinate cooperating writers on one machine. They do not provide a distributed lock, or a crash-atomic replacement of several files.

Batch application

A transaction first acquires writer coordination and checks pending recovery. It checks exact source and target bytes, observed absences, directory inventories and link topology. It creates durable write-ahead metadata before staging every candidate and original backup on the corresponding target filesystem. All stages, backups and preconditions must match before the first destination replacement. Artifact registration requires the predeclared deterministic path to have been absent before creation and its returned bytes to match the requested content. A host-returned path or matching transaction ID does not transfer ownership of an existing file or directory. Directory receipts must be a canonical subset of the parents observed missing in the plan.

The transaction rechecks remaining inputs between installations. Atomic native replacement protects one destination at a time.

If a replacement fails, the transaction attempts rollback in reverse order. RestoredPaths identifies originals proven restored. UnresolvedPaths identifies paths whose state is uncertain or whose bytes no longer match the recorded original or candidate. The result retains artifact paths and their transaction owner. Configuration bytes, backup bytes and secrets are never included in these metadata results.

Written means the complete candidate was installed and its owned temporary artifacts were cleaned. Unchanged leaves equivalent target bytes and timestamps alone. Conflict means input evidence changed. RolledBack means rollback proved the originals restored and cleaned transaction artifacts. RecoveryRequired means retained evidence needs explicit inspection and recovery. Do not treat a partial workspace, successful command exit from an unrelated tool, or the presence of a candidate TOML file as evidence that the batch completed.

Inspect and recover through the shared API

Recovery is explicit. Keep the returned journal path and inspect it before taking action. Use the same authorized storage host that owns the paths:

var transaction = new ConfigurationTransaction(new NativeConfigurationTransactionHost());
var pending = transaction.InspectPending(new[] { workspaceRoot });
foreach (var item in pending)
{
    Console.Error.WriteLine($"{item.JournalPath}: {item.Code}");
    // Show item.Targets, item.Artifacts, item.UnresolvedPaths to the operator.
}

var inspection = transaction.InspectRecovery(journalPath);
if (inspection.Valid && inspection.UnresolvedPaths.IsEmpty)
{
    // This call is an explicit recovery request by the host/operator.
    var outcome = transaction.Recover(journalPath);
    // Check outcome.Status and preserve outcome.Artifacts on RecoveryRequired.
}

Inspection does not change files. Recovery reacquires writer coordination and checks the metadata and file identities again. Without a valid committed witness, recovery restores the recorded originals. A target that was absent is removed only when its current bytes match this transaction’s candidate. With a durable committed witness, recovery verifies all installed candidates and finishes cleanup.

A host can stop between creating a directory and returning its ownership receipt. Inspection reports these paths in UncertainDirectories. Default recovery keeps the journal and returns RecoveryRequired. After reviewing those paths, an operator can explicitly retain them as user-owned directories:

var result = transaction.Recover(journalPath, inspection.UncertainDirectories);
// Check result.Status. result.PreservedDirectories lists directories retained
// by this explicit request; transaction cleanup never claims or removes them.

This option accepts only the journal’s uncertain paths that remain regular directories. Linked paths, file replacements and unrelated paths remain blocked. It does not authorize deletion or discard user content.

If an adapter returns a foreign file or directory, or claims one in an exception, the live result reports that path as unresolved without claiming it as an owned artifact. A separate recovery journal records the rejected receipt. Explicit recovery preserves the unverified external paths, rolls back only verified owned changes, and removes only the transaction’s verified artifacts. A matching owner ID or filename prefix alone never authorizes moving, restoring or deleting a file.

If a target, backup, stage or journal has unexpected content or link topology, recovery preserves it and reports an unresolved path. Live cleanup validates the complete artifact set before deleting any artifact, then rechecks each artifact immediately before deletion. Changing a backup or even reformatting a journal therefore produces RecoveryRequired and retains the guard. Journal bytes must match their canonical versioned encoding, including known roles and ownership paths; whitespace, duplicate/unknown fields and role changes are not silently normalized during recovery. Do not overwrite the user’s new bytes with an old backup or delete evidence to bypass the guard. Preserve both versions, review the metadata and restore a specifically approved state before retrying recovery. Invalid/truncated journals need manual review; automatic recovery does not invent metadata or infer ownership from a filename alone.

Journals and interruption windows

Native .morphir-edit-<transaction-id>-... files contain private stages, original backups and metadata journals. Native creation uses exclusive new files and user read/write permissions. Metadata and file bytes are flushed before proceeding. Journals identify the complete target set, original and candidate hashes, stage and backup paths, and guarded directories. They do not contain the source values. The initial manifest is written in an existing common ancestor before any parent directory creation. A durable directory receipt records directories whose creation the host confirmed. The complete write-ahead manifest allows inspection after interruption before a stage, between staging and replacement, or immediately after a replacement whose callback did not return.

A committed witness distinguishes a completed installation from an interrupted commit. Cleanup preserves this witness until the other owned stages, backups and journals have been removed. An interruption during cleanup therefore remains identifiable. Complete success and complete rollback remove only owned temporary files and empty owned directories. An incomplete outcome retains journals and backups. Cooperating configuration and project/workspace editing paths refuse an overlapping write until explicit recovery resolves that state.

File flushes and atomic replacement do not promise group atomicity or recovery from every filesystem, storage-controller or power-loss failure. Uncooperative processes can modify files outside Morphir’s writer lock. Their unexpected bytes are treated as drift, not silently overwritten during recovery.

Custom storage hosts

IConfigurationTransactionHost remains the storage boundary. Hosts serialize ordered canonical guarded paths across their writers, report exact byte hashes, and atomically install one file. Writable hosts explicitly opt in with SupportsDurableRecovery = true and implement FindRecoveryJournals and EnumerateTransactionArtifacts. Enumeration must remain bounded to authorized locations, include retained transaction-owned artifacts, and never follow links. Stages and backups use ConfigurationTransaction.GetArtifactPath(destination, transactionId, role); recovery validates the owner, destination anchor and role, including when several targets share identical bytes. Returning null for artifact enumeration means recovery cannot prove cleanup completeness and returns config.recovery_host_capability. Older adapters remain source compatible. Application refuses a host without this capability before any write, returning IoFailure and config.recovery_host_capability. Read-only planning remains available. Hosts must add durable enumeration before opting into writes; an empty process-local registry is not a substitute.

Recovery metadata uses version 2. Automatic recovery refuses unknown versions and invalid ownership. Preserve earlier journals for explicit review with their original host/version. Do not rewrite or silently delete them during an upgrade.

A lost host acknowledgement does not prove a commit failed. If the committed witness is present and validates against the attempted operation, application preserves the candidate files and backups and returns recovery required for explicit completion. If the witness is absent, rollback can proceed.

Rejected transaction-shaped artifacts remain recovery blockers while present. Recovery preserves the journals and reports their unresolved paths rather than claiming complete rollback with metadata left behind. Any manual disposition must first establish the artifact’s origin and preserve user changes. Unrelated foreign files are never cleanup authority.

If the storage host disconnects while a failure is being handled, application returns a typed recovery-required result with known or possible evidence locations and unresolved targets. Its fallback does not perform further storage observations or blind rollback. Reconnect storage and inspect the retained journal before explicitly recovering.

15 - Workspace migration API and inventory

Adopt a bounded repository

Morphir.Configuration.IO.WorkspaceMigrationService inventories, plans and applies workspace adoption without constructing the CLI. It reuses ProjectMigration’s mapping, raw loss ledger and independent equivalence verification. It does not compile sources, move generated outputs, remove classic manifests or invoke publication services.

var root = ProjectPath.normalize(Path.GetFullPath(directory));
var service = new WorkspaceMigrationService(
    new FileSystemProjectTree(root), new NativeConfigurationTransactionHost());
var preview = service.Plan(root, includes: ["apps/**"], excludes: ["**/experimental"]);
if (preview.CanApply)
{
    var outcome = service.Apply(preview);
    // Inspect outcome.Status, Artifacts, RestoredPaths and UnresolvedPaths.
}

Omit storage for a read-only host. Supply InMemoryProjectTree or implement IProjectTree for a mounted tree. Both use canonical absolute paths with / separators. A mounted directory returns only immediate child directories or links. A child outside that directory or the selected operation root blocks the plan. Application requires storage-backed evidence and the service that created the verified preview. Read-only and foreign-service plans cannot be applied.

Inventory rules

The absolute directory passed to Plan is the operation boundary even if the host’s authorization boundary is larger. No ancestor is searched for projects or workspace adoption. Inventory orders paths ordinally. It inspects shared primary layouts and formats, including supplemental workspace JSON, without reading personal, machine, environment or active profile overlays.

Include and exclude filters are repeated root-relative segment globs. * and ? match within one segment; ** matches zero or more segments. Absolute paths, empty filters and .. segments are invalid. Excluded directories are pruned. Existing workspace.exclude patterns retain their declared workspace-base anchor and cannot be overridden by an include filter.

Filters select conversions; they do not remove existing declared members. Shared discovery still inspects those members to verify their unchanged ownership and selection. If a retained membership glob would enter a skipped output/dependency tree or nested workspace, the plan reports workspace.migrate_membership_boundary and blocks. Refine the declared membership or exclusions explicitly; migration does not silently narrow a glob to the projects it happened to inventory.

Version control directories .git, .hg, .svn, dependency directories node_modules, elm-stuff, vendor, .nuget, packages, and output directories bin, obj, dist, build, target are skipped. Primary layout directories .morphir and .config are inspected as layouts at their owner; they are not separate projects. The declared workspace output directory is also skipped. Discovered links are reported and never followed, including linked ancestors. A nested declared workspace is reported as a boundary and its children are not inventoried for outer adoption. The maximum inventory is 100,000 directories; exceeding it blocks the plan instead of returning an incomplete applicable result.

Inventory explains candidate, excluded, boundary, already migrated and skipped link paths. Duplicate names remain distinct paths and produce a diagnostic; explicit path selection remains unambiguous.

Reviewed membership and verification

Members preserves existing declarations, including globs, order and defaults, and appends explicit root-relative paths for additions. A root project remains a root project and never becomes its own glob member. An otherwise projectless root becomes virtual. Already migrated members retain their bytes and settings. An existing valid workspace with no conversions succeeds unchanged. An undeclared empty root reports that adoption is impossible.

Projects contains the single-project plans and their loss ledgers. Use each project’s Presentation for logs. The canonical Plan.Candidate and Plan.Edits contain application values, including secrets; do not log them. Secret references remain opaque leaves. Unsupported values cannot be dropped to make a batch pass. The classic source contract is the same as project migration: a supported .json manifest can contain full accepted JSON5 syntax. Legacy-shaped primaries in other extensions are reported as blockers by the project service, rather than falsely labeled already migrated. Canonical multi-format primaries remain readable.

Every conversion receives explicit proposed base workspace context. Verification disables activation and compares declared profile controls separately. The aggregate candidate excludes every converted classic source together and reruns shared discovery and distinct-path selection. It must preserve project source paths and non-adoption settings. Intended ownership, membership, default selection and shared output-root changes appear separately in AdoptionEffects. They do not authorize other value changes. Existing generated files stay where they are.

Root classic conversion uses the original target for conflict and loss checks, then composes the reviewed member additions in the common workspace context. Existing conflicting membership values still block. Declared workspace bases and output roots do not reanchor the root project’s source directory. The additive ProjectMigrationService.PlanInWorkspace API supplies component plans for this composition; these read-only components cannot bypass aggregate application.

Application and recovery

Application validates source bytes, target bytes or absence, relevant directory listings and primary topology. It stages the entire batch before replacing any destination. Cooperating writers coordinate through the shared transaction adapter. Each file installation is atomic; the batch is not crash-atomic.

Storage-backed previews inspect pending journals without writing and expose PendingRecovery; a pending operation makes the preview inapplicable. On failure inspect the structured status, restored paths, unresolved paths and owned artifacts. Pending recovery blocks overlapping shared configuration edits and project migrations as well as subsequent workspace writes. Use the explicit shared journal inspection and recovery APIs described in transaction recovery. Do not remove recovery journals or backups to bypass a guard. Preserve user changes and unresolved evidence.

CLI reports use the same plans and outcomes. See the workspace command reference, walkthrough and troubleshooting for exit codes, previews and scriptable JSON.

Supplied exclusions also control root-project conversion. For example, excluding . leaves the classic root project in place while child adoption can still add a workspace declaration. Root-project identity and existing ownership are preserved. Excluding ** selects no conversions, including at the root.

Every newly adopted project is checked against its original non-adoption values, including additions to an existing workspace. A root backend or other inherited setting cannot silently change the new member. Retained members keep their prior workspace context; new members report their original standalone ownership and local output root before adoption.

16 - Contributing

How to contribute to Morphir .NET - guidelines, setup, and design documentation

Thank you for your interest in contributing to Morphir .NET! This section provides everything you need to get started as a contributor.

Quick Start

  1. Fork the repository
  2. Clone your fork
  3. Set up your development environment
  4. Create a branch for your changes
  5. Submit a pull request

Development Setup

Prerequisites

  • .NET SDK 11.0 or higher (the version in global.json)
  • Git

Build & Test

# Build the project
dotnet build

# Run tests
dotnet test --nologo

# Format code (required before committing)
dotnet format

Install Git Hooks

dotnet tool restore
dotnet husky install

Coding Standards

  • Follow the existing code style
  • Use C# 14 / F# 9 features where appropriate
  • Prefer immutable data structures
  • Write comprehensive tests (TDD approach)
  • Update documentation as needed
  • Follow AGENTS.md for architectural guidance

Pull Request Process

  1. Ensure all tests pass: dotnet test --nologo
  2. Run code formatters: dotnet format
  3. Update documentation if needed
  4. Create a focused PR with a clear description
  5. Follow Conventional Commits format
  6. Ensure DCO is signed (see CONTRIBUTING.md)

Need Help?

Key Resources

ResourceDescription
AGENTS.mdComprehensive guidance for AI agents and developers
CONTRIBUTING.mdDCO and legal requirements
Code of ConductCommunity guidelines

16.1 - Design Documentation

Design documents, PRDs, and architectural specifications for Morphir .NET

This section contains design documentation for Morphir .NET, including AI Skill Framework architecture, Product Requirements Documents, and architectural decision records.

AI Skill Framework

The morphir-dotnet project uses a sophisticated AI skill framework (gurus) for cross-agent development assistance:

DocumentDescription
Skill Framework DesignComprehensive architecture for unified, cross-agent AI skills
Guru PhilosophyThe collaborative AI stewardship philosophy behind morphir-dotnet gurus
Guru Creation GuideStep-by-step guide for creating new AI gurus
Technical Writer SkillRequirements for the Technical Writer skill

Guru Specifications & Enhancements

DocumentDescription
Issue #240 Enhancement SummaryQuick reference for Elm-to-F# Guru enhancement with guru framework principles
Issue #240 Enhanced SpecificationComplete specification for Elm-to-F# Guru with proactive review, token efficiency, and maturity phases
Issue #240 Navigation GuideNavigation guide for all Issue #240 enhancement documents

Product Requirements Documents

PRDs track feature requirements, design decisions, and implementation status:

Design Process

For Standard Features

  1. PRD Creation: Major features start with a comprehensive PRD
  2. Review & Approval: PRDs are reviewed before implementation begins
  3. Implementation: PRDs are updated with implementation notes as work progresses
  4. Completion: Completed PRDs serve as historical reference

For AI Skills & Gurus

  1. Philosophy First: Understand guru principles before design
  2. Framework Definition: Follow skill framework architecture
  3. Review Capability: Every guru includes proactive review capability
  4. Cross-Agent Design: Ensure portability across Claude, Copilot, Cursor, and other agents
  5. Retrospective Integration: Plan for continuous improvement through feedback loops

16.1.1 - AI Skill Framework Design

Design for unified, cross-agent AI skill architecture (gurus)

AI Skill Framework Design

Overview

This document establishes a comprehensive, scalable architecture for AI skills (known as “gurus” in this project) that work seamlessly across Claude Code, GitHub Copilot, and other coding agents. The goal is to create a repeatable pattern for developing specialized AI team members who improve continuously and provide expert guidance in specific domains.

Motivation

The morphir-dotnet project has implemented three sophisticated gurus (QA Tester, AOT Guru, Release Manager) that provide specialized expertise through:

  • Decision trees for problem-solving
  • Automation scripts (F#) for repetitive tasks
  • Playbooks for complex workflows
  • Templates for common scenarios
  • Pattern catalogs of domain knowledge

As the project plans to add more gurus (Elm-to-F# Guru, Documentation Guru, Security Guru, etc.), we need:

  1. A clear definition of what makes a guru
  2. Repeatable patterns for creation
  3. Cross-agent accessibility (not Claude-only)
  4. Continuous improvement mechanisms
  5. Cross-project reuse strategy

What is a Guru?

A guru is not a tool or a prompt. It’s a knowledge stewardship system with these characteristics:

mindmap
  root((Guru))
    Stewardship
      Owns a domain
      Accountable for quality
      Quality gate
    Continuous Improvement
      Learns from interactions
      Quarterly reviews
      Feedback loops
    Proactive Review
      Scans for issues
      Detects problems early
      Captures patterns
    Automation-First
      F# scripts
      Reduces token cost
      Improves with scale
    Collaboration
      Clear hand-offs
      Escalation paths
      Shared patterns

Stewardship

  • Owns a domain (Quality, Optimization, Releases, Migration, etc.)
  • Accountable for quality, velocity, and responsibility in that domain
  • Maintains and evolves best practices and decision frameworks
  • Acts as a quality gate preventing regressions and anti-patterns

Continuous Improvement

  • Learns from interactions - Every session captures patterns and discoveries
  • Feeds back into guidance - Playbooks, templates, and catalogs evolve
  • Automated feedback loops (e.g., Release Manager retrospectives)
  • Quarterly reviews ensure knowledge remains current

Proactive Review

  • Scans the domain regularly for issues, violations, and improvement opportunities
  • Detects problems before they escalate - Review findings become preventative actions
  • Captures patterns and trends - Quarterly reviews identify what’s working and what’s not
  • Feeds review findings into automation - Patterns discovered 3+ times become scripts
  • Combines with retrospectives for continuous improvement: Find problems → Fix them → Prevent them → Improve guidance

Example: AOT Guru’s Quarterly Review

  • Scans all projects for reflection usage (IL2026 patterns)
  • Measures binary sizes vs. targets
  • Reports: “3 new reflection patterns, 1 binary growing too fast”
  • Actions: Update decision tree, create detection script, monitor closely

Automation-First

  • Identifies high-token-cost tasks - Repetitive diagnostics, testing, validation
  • Creates F# scripts to automate these patterns
  • Reduces cognitive load for future sessions
  • Improves with scale - Every use makes the system smarter

Collaboration

  • Coordinates transparently with other gurus
  • Clear hand-offs at domain boundaries
  • Escalates decisions beyond scope to maintainers
  • Leverages shared patterns from .agents/ guidance

Example: Release Manager

The Release Manager guru exemplifies this philosophy:

  • Stewardship: Owns release lifecycle and process consistency
  • Continuous Improvement: Automated retrospective system captures feedback on failures/successes
  • Automation: monitor-release.fsx polls autonomously, saving tokens per release
  • Collaboration: Hands off to QA Tester for verification; coordinates with Elm-to-F# on version tracking

Architecture

The skill framework is organized in layers, from universal guidance accessible to all agents down to Claude-specific enhancements.

graph TB
    subgraph "Layer 4: Meta-Guidance"
        META[".agents/guru-*.md<br/>Philosophy & Creation Guide"]
    end

    subgraph "Layer 3: Claude Enhancement"
        SKILLS[".claude/skills/<br/>QA Tester | AOT Guru | Release Manager"]
    end

    subgraph "Layer 2: Agent Bridging"
        COPILOT["copilot-instructions.md"]
        CLAUDEMD["CLAUDE.md"]
    end

    subgraph "Layer 1: Universal Guidance"
        AGENTS["AGENTS.md + .agents/"]
    end

    META --> SKILLS
    SKILLS --> CLAUDEMD
    AGENTS --> COPILOT
    AGENTS --> CLAUDEMD

    style META fill:#e1f5fe,stroke:#01579b
    style SKILLS fill:#fff3e0,stroke:#e65100
    style COPILOT fill:#f3e5f5,stroke:#7b1fa2
    style CLAUDEMD fill:#f3e5f5,stroke:#7b1fa2
    style AGENTS fill:#e8f5e9,stroke:#2e7d32

Layer 1: Universal Guidance (All Agents)

Files: AGENTS.md, .agents/

This layer provides tool-agnostic guidance applicable to all agents:

  • Primary authority for coding standards, practices, philosophy
  • Decision frameworks and playbooks
  • Testing strategy, TDD workflow, quality standards
  • Morphir IR principles and modeling
  • Size: ~169 KB (AGENTS.md + 3 .agents/ guides)

Audience: Claude Code, GitHub Copilot, Cursor, Windsurf, Aider, Neovim+Codeium, human developers

Layer 2: Agent-Specific Bridging

Files: copilot-instructions.md (Copilot), CLAUDE.md (Claude Code)

This layer provides agent-specific features and configuration:

  • How to access universal guidance in each agent
  • Agent-specific capabilities and limitations
  • Links to skills and automation scripts
  • Size: ~150 KB each (consolidated from 353 KB and 307 KB)

Audience: Copilot users and Claude Code users respectively

Layer 3: Claude Code Enhancement

Files: .claude/skills/

This layer provides Claude-only specialization:

  • 3 stable gurus: QA Tester, AOT Guru, Release Manager
  • 1 planned: Elm-to-F# Guru
  • Accessible via @skill {skill-name} syntax
  • YAML metadata with trigger keywords
  • Size: ~220+ KB for 3 skills, framework designed to scale to 5-10+

Audience: Claude Code users only

Gurus:

  • QA Tester - Testing, validation, regression prevention (31 KB)
  • AOT Guru - Optimization, trimming, AOT readiness (220 KB)
  • Release Manager - Release lifecycle, deployment, recovery (104 KB)
  • Elm-to-F# Guru (planned) - Elm-to-F# migration, code generation (TBD)

Layer 4: Meta-Guidance (New)

Files: .agents/guru-philosophy.md, .agents/guru-creation-guide.md, .agents/skill-matrix.md

This layer guides the creation and evolution of gurus:

  • Guru philosophy and principles
  • Step-by-step creation guide
  • Maturity and coordination matrix
  • Success criteria and learning systems

Audience: Future skill creators, maintainers, all agents

Skill Anatomy

Each guru skill follows a standard structure with well-defined components:

graph LR
    subgraph "Skill Directory"
        direction TB
        SKILL["skill.md<br/>Main Persona"]
        README["README.md<br/>Quick Start"]
        MAINT["MAINTENANCE.md<br/>Review Process"]
    end

    subgraph "Scripts/"
        S1["automation-1.fsx"]
        S2["automation-2.fsx"]
        S3["common.fsx"]
    end

    subgraph "Templates/"
        T1["decision-template.md"]
        T2["workflow-template.md"]
    end

    subgraph "Patterns/"
        P1["pattern-1.md"]
        P2["pattern-2.md"]
        P3["...discovered over time"]
    end

    SKILL --> Scripts/
    SKILL --> Templates/
    SKILL --> Patterns/

    style SKILL fill:#fff3e0,stroke:#e65100
    style README fill:#e8f5e9,stroke:#2e7d32
    style MAINT fill:#e1f5fe,stroke:#01579b

Standard Components

Each guru skill consists of:

ComponentPurposeSizeAudience
skill.mdMain persona, competencies, decision trees, playbooks1000-1200 lines (~50 KB)Claude Code via @skill
README.mdQuick start guide, use cases, script reference300-400 lines (~16 KB)All agents (readable on GitHub)
Scripts/Diagnostic, testing, validation F# scripts3-5 scripts, 15-20 KB eachAll agents (runnable via terminal)
Templates/Issue templates, test templates, workflow templatesVariableAll agents (reusable)
Patterns/Domain-specific pattern catalogCumulativeAll agents (readable)
MAINTENANCE.mdQuarterly review process, feedback capture1-2 KBMaintainers, skill evolvers

Token Budget

Per-Skill Target: 50-100 KB

  • Preferred: 50-75 KB (efficient for context windows)
  • Acceptable: 75-100 KB (comprehensive domains)
  • Large: 100+ KB (complex domains, consider splitting)

Rationale:

  • Claude Code has ~100K token context, can accommodate 200+ KB of skills
  • GitHub Copilot has ~8K tokens for instructions; scripts must be external
  • Other agents balance comprehensiveness with performance

Automation Scripts

F# scripts should identify and automate high-token-cost repetitive work:

Examples:

  • Release Manager’s monitor-release.fsx - Autonomous workflow polling (saves tokens vs. manual polling)
  • QA Tester’s smoke-test.fsx - Quick validation in ~2 minutes (fast feedback loop)
  • AOT Guru’s aot-diagnostics.fsx - Automated problem analysis (reduces diagnostic overhead)

Savings Analysis:

  • Diagnostic script that saves 100-200 tokens per use
  • If used 5 times per quarter: 500-1000 tokens saved per quarter
  • Over 1 year: 2000-4000 tokens saved
  • If skill is 50 KB (~8000 tokens), script pays for itself in 6-12 months

Guru Philosophy

Core Principles

  1. Stewardship, Not Tooling

    • Gurus own domains, not just answer questions
    • Improve with every interaction
    • Accountable for quality in their area
  2. Automate High-Token-Cost Work

    • Identify repetitive diagnostic/testing/validation tasks
    • Create F# scripts to automate them
    • Reduce cognitive load for future sessions
  3. Learn from Every Interaction

    • Document new patterns discovered
    • Update playbooks and catalogs
    • Feed improvements back into guidance
  4. Collaborate Transparently

    • Clear hand-offs to other gurus
    • Explicit coordination points
    • Escalate when beyond scope
  5. Quality/Velocity/Responsibility Balance

    • Maintain or improve code quality
    • Accelerate delivery through automation
    • Take responsibility for domain health

Feedback Mechanisms

Release Manager (Exemplar):

  • Failure Retrospective: When release fails, automatically prompt for feedback
    • Captures: “What went wrong?” and “How to prevent?”
    • Stores in tracking issue for pattern analysis
  • Success Feedback: After 3+ consecutive successes, prompt for improvements
    • Captures: “What could we improve?” and “What automated?”
    • Feeds into playbook refinements
  • Process Change Detection: When release procedures change, prompt for documentation updates

Elm-to-F# Guru (Planned):

  • Pattern Discovery: Every migration discovers new Elm-to-F# patterns
    • Adds to pattern catalog if novel
    • Tags as “Myriad plugin candidate” if repetitive
  • Quarterly Review: Assess patterns, create Myriad plugins for repetitive cases
    • Q1: Document new patterns
    • Q2: Create Myriad plugins (1+ per quarter target)
    • Q3: Update decision trees
    • Q4: Plan next quarter

Template for New Gurus:

  • Identify feedback triggers (when to capture data)
  • Define feedback storage (GitHub tracking issue, IMPLEMENTATION.md, etc.)
  • Establish review schedule (quarterly, per-session, after N uses)
  • Create improvement loop (feedback → updates → publish)

Cross-Agent Compatibility

Claude Code Users

  • Access: @skill {skill-name} syntax activates guru
  • Context Window: ~100K tokens, can load full skill.md + README.md + scripts overview
  • Benefit: Natural invocation, deep expertise, triggers via keywords
  • Example: User mentions “AOT warnings” → AOT Guru automatically invoked with decision trees

GitHub Copilot Users

  • Access: Read .agents/ guides (universal guidance) + .agents/skills-reference.md (skill overview)
  • Automation: Run scripts via terminal: dotnet fsi .claude/skills/{skill}/script.fsx
  • Context Window: ~8K tokens for instructions; must reference external resources
  • Benefit: Same patterns and automation scripts, different discovery mechanism
  • Example: Copilot user reads .agents/qa-testing.md + runs validate-packages.fsx directly

Other Agents (Cursor, Windsurf, Aider, etc.)

  • Access: Read AGENTS.md and .agents/ guides from GitHub
  • Automation: Execute F# scripts directly using dotnet fsi
  • Context Window: Varies (typically 4-20K for instructions)
  • Benefit: Universal guidance, portable scripts, no vendor lock-in
  • Example: Cursor user copies .agents/aot-optimization.md instructions into project context

Capabilities Matrix

CapabilityClaude CodeCopilotCursor/WindsurfOther Agents
@skill syntax✅ Yes❌ No❌ No❌ No
YAML triggers✅ Yes❌ No❌ No❌ No
Read .agents/✅ Yes✅ Yes✅ Yes✅ Yes
Run F# scripts✅ Yes✅ Yes✅ Yes✅ Yes
Decision trees✅ Full context⚠️ Manual reference✅ Yes✅ Yes
Context budget100K+8K4-20K4-20K

The following diagram shows the current and planned guru ecosystem with their coordination relationships:

graph TB
    subgraph "Current Gurus"
        QA["🧪 QA Tester<br/>Testing & Validation"]
        AOT["⚡ AOT Guru<br/>Optimization"]
        RM["📦 Release Manager<br/>Deployment"]
    end

    subgraph "Planned Gurus"
        ELM["🔄 Elm-to-F# Guru<br/>Migration"]
        DOC["📚 Documentation Guru<br/>Docs Quality"]
        SEC["🔒 Security Guru<br/>Security Reviews"]
    end

    QA <-->|"Post-release<br/>verification"| RM
    AOT <-->|"AOT-compatible<br/>tests"| QA
    ELM -->|"Verify AOT<br/>compatibility"| AOT
    ELM -->|"Verify test<br/>coverage"| QA
    DOC -.->|"Pattern<br/>documentation"| ELM
    SEC -.->|"Cross-cuts all"| QA
    SEC -.->|"Cross-cuts all"| AOT
    SEC -.->|"Cross-cuts all"| RM

    style QA fill:#e8f5e9,stroke:#2e7d32
    style AOT fill:#fff3e0,stroke:#e65100
    style RM fill:#e1f5fe,stroke:#01579b
    style ELM fill:#fce4ec,stroke:#c2185b
    style DOC fill:#f3e5f5,stroke:#7b1fa2
    style SEC fill:#ffebee,stroke:#c62828

Current Gurus

QA Tester

  • Domain: Testing, validation, regression prevention
  • Competencies: Test planning, automation, coverage tracking, bug reporting
  • Integration: Coordinates with Release Manager for post-release verification
  • Token Cost: 31 KB (skill + scripts)
  • Portability: High (could apply to morphir-elm, morphir core)

AOT Guru

  • Domain: Optimization, trimming, AOT readiness
  • Competencies: Diagnostics, size optimization, source generators, Myriad expertise
  • Integration: Coordinates with QA Tester for AOT-compatible test runs
  • Token Cost: 220 KB (skill + 3 diagnostic scripts)
  • Portability: High (portable if .NET versions of other projects emerge)

Release Manager

  • Domain: Release lifecycle, deployment, recovery, process improvement
  • Competencies: Version management, changelog handling, deployment monitoring, retrospectives
  • Integration: Coordinates with QA Tester for post-release verification
  • Token Cost: 104 KB (skill + 6 automation scripts)
  • Portability: Medium (could adapt for mono-repo versioning)

Planned Guru

Elm-to-F# Guru (#240)

  • Domain: Elm-to-F# migration, code generation, pattern discovery
  • Competencies: Language expertise, Myriad mastery, test extraction, compatibility verification
  • Integration: Coordinates with AOT Guru for AOT compatibility of generated code
  • Token Cost: TBD (target 50-100 KB)
  • Portability: Medium (patterns portable, IR-specific knowledge less so)

Future Candidates

Documentation Guru

  • Domain: Documentation quality, API docs, examples
  • Competencies: Technical writing, markdown standards, doc generation, accessibility
  • Integration: Coordinates with Elm-to-F# for pattern documentation

Security Guru

  • Domain: Security reviews, threat modeling, compliance
  • Competencies: Vulnerability scanning, OWASP standards, authorization patterns
  • Integration: Cross-cuts all gurus (every skill needs security review)

Performance Guru

  • Domain: Benchmarking, profiling, optimization
  • Competencies: Performance testing, bottleneck identification, optimization strategies
  • Integration: Coordinates with AOT Guru on runtime performance

Token Efficiency Strategy

Problem

GitHub Copilot instruction file is at practical size limit (~28 KB, 56% of available tokens). Cannot add more content without removing something.

Solution: Consolidation & Linking

  1. Remove Duplication (~50 KB savings)

    • copilot-instructions.md: 353 → ~150 lines
    • CLAUDE.md: 307 → ~150 lines
    • Remove duplicated sections about TDD, conventions, Morphir modeling
  2. Cross-Reference Instead of Duplicate

    • Copilot instructions → Link to AGENTS.md Section 9 (TDD)
    • CLAUDE.md → Reference .agents/ guides instead of duplicating content
    • Result: Free up 100-150 KB
  3. Automation Over Explanation

    • High-token-cost work → F# scripts (Release Manager’s polling script)
    • Complex decisions → Guidance docs
    • Result: Reduce explanation overhead
  4. Semantic Linking (Copilot)

    • Include GitHub URLs to full guides
    • Copilot users can follow links for comprehensive details
    • Instructions remain under 8K tokens, full content accessible

Example: Release Manager

Before (Copilot): Full playbooks (1200+ lines, 53 KB)

  • All release workflows documented in instructions
  • Exceeds Copilot token budget significantly
  • Difficult to maintain

After (Copilot):

  • Overview in instructions (~500 lines, ~20 KB)
  • Link to .claude/skills/release-manager/skill.md for details
  • Link to .agents/skills-reference.md#release-manager for cross-agent access
  • monitor-release.fsx handles polling autonomously (reduces explanation)
  • Result: 60%+ token savings while maintaining capability

Savings Calculation

Release Manager Skill:
- Playbook explanation: 1200 lines → 300 lines (75% reduction)
- Reason: Automation handles complex logic (monitor-release.fsx)
- Savings: 100-150 KB in copilot-instructions.md
- Tradeoff: Users must read .agents/skills-reference.md for full playbooks
- Benefit: Copilot users still get guidance, just discover it differently

Cross-Project Reuse

Portability Strategy

Portable Skills:

  • QA Tester → morphir-elm, morphir core (testing patterns apply universally)
  • AOT Guru → morphir-elm (if .NET port emerges)

Partially Portable:

  • Release Manager → Could adapt for mono-repo versioning (CHANGELOG format may differ)
  • Elm-to-F# Guru → Pattern catalog portable, IR-specific knowledge less so

Reuse Checklist

When planning to use a guru in a new project:

  • Understand skill’s domain and scope
  • Assess project-specific config needs
  • Identify paths/repos that need adjustment
  • Read “Adapt to New Project” section in skill README
  • Test skill with sample scenario
  • Document adaptations (if any)
  • Report improvements back to origin project

Example: QA Tester in morphir-elm

Original (.morphir-dotnet): `.claude/skills/qa-tester/`
├── skill.md - Core QA philosophy, no project-specific content
├── README.md - Scripts references can be adapted
└── scripts/
    ├── smoke-test.fsx - Paths would need adjustment
    ├── regression-test.fsx - Test command would change
    └── validate-packages.fsx - Package names would differ

Adapted (.morphir-elm):
├── Test: npm run test vs. dotnet test
├── Smoke: npm run build vs. dotnet build
├── Packages: npm packages vs. NuGet packages
├── Regression: Same BDD/TDD philosophy, different tech stack

Effort: 2-4 hours to adapt and test

Future Expansion

Roadmap

timeline
    title Guru Framework Roadmap
    section Phase 1 - Now
        3 stable gurus proven : QA Tester, AOT Guru, Release Manager
        Framework documented : Skill Framework Design
        Cross-agent accessibility : In progress
    section Phase 2 - Q1 2026
        Elm-to-F# Guru : Issue #240
        Code generation project : Issue #241
        Quarterly reviews : Established
    section Phase 3 - Q2-Q3 2026
        Documentation Guru : Planned
        Security Guru : Planned
        Cross-project reuse : QA Tester → morphir-elm
    section Phase 4 - Future
        5-10+ gurus : Actively maintained
        Skill marketplace : Envisioned
        Continuous improvement : Culture embedded

Phase 1 (Now):

  • ✅ 3 stable gurus proven effective
  • ✅ Skill framework documented
  • 🚧 Cross-agent accessibility implemented
  • 🚧 Guru creation guide created

Phase 2 (Q1 2026):

  • Elm-to-F# Guru implemented (#240)
  • Morphir.Internal.CodeGeneration created (#241)
  • Skills integrated with code generation
  • Quarterly review process established

Phase 3 (Q2-Q3 2026):

  • Documentation Guru planned
  • Security Guru planned
  • First cross-project reuse (QA Tester → morphir-elm)
  • Skill marketplace envisioned

Phase 4 (Future):

  • 5-10+ gurus actively maintained
  • Cross-project skill sharing established
  • Guru coordination at scale proven
  • Continuous improvement culture embedded

Scaling Considerations

Guru Coordination at Scale:

Current (3 gurus):
QA Tester ↔ Release Manager ↔ AOT Guru

Future (7 gurus):
Documentation ← Elm-to-F# → AOT → QA ↔ Release
       ↓
   Security (cross-cuts all)

Dependency Management:

  • Explicit coordination graph (who coordinates with whom)
  • Hand-off protocols at boundaries
  • Error handling for coordination failures
  • Token budgets account for coordination overhead

Feedback Loop Management:

  • Each guru’s retrospective/review process documented
  • Aggregated insights shared quarterly
  • Cross-guru learning captured (patterns that cross domains)

Success Criteria

For the Framework

  • Architecture document complete
  • GitHub issues created for implementation
  • Guru philosophy widely understood
  • Skill creation guide enables new gurus
  • 3 existing gurus assessed for alignment
  • Cross-agent accessibility proven
  • First new guru (Elm-to-F# #240) created using framework
  • Quarterly review process established and running
  • Token efficiency targets met (Copilot <30 KB)

For New Gurus

  • 3+ core competencies defined
  • 3-5 automation scripts created
  • 20+ patterns in catalog
  • Feedback mechanism implemented
  • Coordination points with other gurus explicit
  • Cross-project portability assessed
  • Quarterly review schedule established
  • Cross-agent compatibility documented

References

  • #253 - Design: Unified Cross-Agent AI Skill Framework Architecture
  • #254 - Implement: Cross-Agent Skill Accessibility & Consolidation
  • #255 - Implement: Guru Creation Guide & Skill Template
  • #240 - Create Elm to F# Guru Skill
  • #241 - Create Morphir.Internal.CodeGeneration Project

Last Updated: December 19, 2025 Maintained By: @DamianReeves Version: 1.0 (Initial Release)

16.1.2 - Guru Philosophy

The collaborative AI stewardship philosophy behind morphir-dotnet gurus

Guru Philosophy

The Core Concept

A guru is not a tool. It’s not a utility function or a helpful prompt. A guru is a knowledge stewardship system—a specialized AI team member who owns a domain, improves continuously, and acts as a collaborative partner in advancing project health, maintainability, and velocity.

graph LR
    subgraph "Traditional AI Helper"
        Q1[Question] --> A1[Answer]
        Q2[Question] --> A2[Answer]
        Q3[Question] --> A3[Answer]
    end

    subgraph "Guru Philosophy"
        I[Interaction] --> L[Learning]
        L --> K[Knowledge Base]
        K --> G[Better Guidance]
        G --> I
    end

    style Q1 fill:#ffcdd2
    style Q2 fill:#ffcdd2
    style Q3 fill:#ffcdd2
    style I fill:#c8e6c9
    style L fill:#c8e6c9
    style K fill:#c8e6c9
    style G fill:#c8e6c9

This philosophy distinguishes morphir-dotnet’s approach to AI collaboration from the typical “ask the AI for help with X” pattern.

The Guru is Not…

Not a Tool

  • ❌ Tools are static; gurus evolve
  • ❌ Tools answer one question; gurus build knowledge systems
  • ❌ Tools don’t improve themselves; gurus have feedback loops
  • ✅ Gurus capture patterns and feed them back into guidance

Not a One-Off Helper

  • ❌ One-off helpers solve today’s problem; gurus solve today’s and tomorrow’s
  • ❌ One-off helpers forget; gurus learn
  • ❌ One-off helpers don’t coordinate; gurus collaborate
  • ✅ Gurus establish playbooks that improve with experience

Not a Replacement for Human Judgment

  • ❌ Gurus don’t make decisions beyond their scope
  • ❌ Gurus don’t override human preferences without explanation
  • ✅ Gurus escalate when uncertain
  • ✅ Gurus provide guidance and let humans decide

The Guru Is…

A Domain Steward

A guru owns a specific area of the project:

  • Quality Steward (QA Tester) - Maintains testing standards and regression prevention
  • Optimization Steward (AOT Guru) - Guards trimming goals and AOT readiness
  • Process Steward (Release Manager) - Ensures releases are reliable and predictable
  • Migration Steward (Elm-to-F# Guru) - Preserves fidelity and quality in cross-language migration

What stewardship means:

  • Accountable for quality in the domain
  • Proactive, not reactive (“What problems can I prevent?”)
  • Maintains best practices and decision frameworks
  • Improves gradually, with intention

A Learning System

A guru improves over time through automated feedback:

flowchart TD
    subgraph "Continuous Learning Cycle"
        A[Session/Interaction] --> B{New Pattern<br/>Discovered?}
        B -->|Yes| C[Document Pattern]
        B -->|No| D[Apply Existing<br/>Patterns]
        C --> E[Update Playbooks]
        D --> F[Track Effectiveness]
        E --> G[Quarterly Review]
        F --> G
        G --> H{Pattern Repeated<br/>3+ Times?}
        H -->|Yes| I[Create Automation<br/>Script]
        H -->|No| J[Continue Monitoring]
        I --> K[Permanent<br/>Improvement]
        J --> A
        K --> A
    end

    style A fill:#e3f2fd
    style C fill:#c8e6c9
    style E fill:#c8e6c9
    style I fill:#fff9c4
    style K fill:#c8e6c9

Release Manager Example (Proof):

  • After every release failure → Automated retrospective captures “What went wrong?” and “How to prevent?”
  • After 3+ consecutive successes → Prompts for improvement ideas
  • When release procedures change → Detects and prompts playbook updates
  • Result: Release playbooks evolve each quarter, getting smarter

Elm-to-F# Guru Example (Planned):

  • Every migration discovers new Elm-to-F# patterns
  • Patterns repeated 3+ times trigger “Create Myriad plugin?” decision
  • Quarterly reviews identify automation opportunities
  • Pattern catalog grows; decision trees improve

Key principle: “Feedback is built in. Learning is automatic.”

An Automation Specialist

A guru identifies high-token-cost repetitive work and automates it:

Release Manager’s monitor-release.fsx:

  • Manual: Check GitHub Actions every few minutes (many tokens)
  • Automated: Script polls autonomously, reports status (few tokens)
  • Savings: 50-100 tokens per release
  • Over 20 releases/year: 1000-2000 tokens saved

QA Tester’s regression-test.fsx:

  • Manual: Run tests manually, interpret results (tokens)
  • Automated: Script runs full test suite, reports coverage (few tokens)
  • Savings: Medium tokens per session

AOT Guru’s aot-diagnostics.fsx:

  • Manual: Read IL warnings, categorize, suggest fixes (tokens)
  • Automated: Script parses logs, categorizes, suggests (few tokens)
  • Savings: Medium-high tokens per use

Philosophy: “Every high-token task automated is a permanent improvement.”

A Collaborator

A guru coordinates transparently with other gurus:

sequenceDiagram
    participant ELM as Elm-to-F# Guru
    participant AOT as AOT Guru
    participant QA as QA Tester
    participant RM as Release Manager

    ELM->>AOT: Generated code for review
    Note over AOT: Verify AOT compatibility
    AOT-->>ELM: ✓ AOT-safe + suggestions

    ELM->>ELM: Apply recommendations

    ELM->>QA: Code ready for testing
    Note over QA: Run test suite
    QA-->>ELM: ✓ Coverage 85%

    ELM->>RM: Feature complete
    Note over RM: Include in release
    RM-->>ELM: ✓ Scheduled for v1.2.0

Collaboration principles:

  • Explicit hand-offs at domain boundaries
  • Clear communication of status and constraints
  • Escalation paths when uncertain
  • Mutual respect for expertise

A Reviewer

A guru proactively reviews the codebase and ecosystem for quality, adherence to principles, and opportunities:

Review Scope (Domain-Specific):

  • QA Tester reviews: Test coverage, regression gaps, missing edge cases, BDD scenario compliance
  • AOT Guru reviews: Reflection usage, trimming-unfriendly patterns, AOT compatibility, binary size creep
  • Release Manager reviews: Release process adherence, changelog quality, version consistency, automation opportunities
  • Elm-to-F# Guru reviews: Migration patterns, Myriad plugin opportunities, F# idiom adherence, type safety

Review as Proactive Stewardship:

  • Gurus don’t wait to be asked; they scan for issues regularly
  • Review reports highlight problems AND suggest fixes
  • Reviews become input to retrospectives (“We found X issues this quarter”)
  • Findings feed back into automation (e.g., “Reflection pattern appears 5 times → Create Myriad plugin”)

Key difference from one-off code review:

  • Code review is reactive: “Please review my PR”
  • Guru review is proactive: “I scanned the codebase and found these issues”
  • Code review gives feedback once
  • Guru review captures findings to improve guidance

Example: AOT Guru’s Quarterly Review

AOT Guru runs aot-scan.fsx quarterly against all projects:
├── Detects reflection usage (IL2026 patterns)
├── Measures binary sizes vs. targets
├── Identifies new trimming-unfriendly patterns
├── Documents findings in quarterly report
└── Feeds findings into:
    ├── Playbooks (e.g., "We found 3 new reflection anti-patterns")
    ├── Decision trees (e.g., "When is Myriad worth it?")
    ├── Automation (e.g., "Script now detects this pattern")
    └── Next quarter's review criteria

Integration with Retrospectives:

  • Retrospectives answer: “What went wrong? How do we prevent it?”
  • Reviews answer: “What issues exist right now? What patterns are emerging?”
  • Together: Continuous improvement (see problems → fix them → prevent them → improve guidance)

A Teacher

A guru documents and preserves knowledge:

  • Decision trees for problem-solving
  • Pattern catalog of domain-specific examples
  • Playbooks for complex workflows
  • Templates for common scenarios
  • “Why?” explanations, not just “What?”

Example: AOT Guru teaches

  • “IL2026 warnings indicate reflection. Here’s why that matters for AOT.”
  • “Myriad can generate JSON codecs at compile-time. Here’s when to use it.”
  • “Source generators work differently than runtime reflection. Here’s the trade-off.”

The Guru Philosophy in Action

Release Manager: The Exemplar

The Release Manager guru embodies the full philosophy:

Stewardship:

  • Owns the release process and its reliability
  • Accountable for release quality and consistency
  • Proactively prevents failures

Learning:

  • Captures failure retrospectives automatically
  • After 3 successes, prompts for improvements
  • Detects process changes and updates playbooks
  • Playbooks improve every quarter

Review:

  • Quarterly review of all releases: Timing, success rate, common issues
  • Scans release artifacts for naming inconsistencies, changelog quality
  • Detects automation opportunities (e.g., “Failed at same step 3 times, automate this”)
  • Report feeds into playbooks: “We saw X failure pattern, added prevention step”

Automation:

  • monitor-release.fsx polls GitHub Actions autonomously
  • prepare-release.fsx validates pre-flight conditions
  • validate-release.fsx verifies post-release success
  • resume-release.fsx handles failure recovery
  • Total: 6 scripts handling routine release logistics

Collaboration:

  • Coordinates with QA Tester for post-release verification
  • Coordinates with all gurus on version tagging
  • Clear escalation: Maintainer reviews if humans need to intervene

Teaching:

  • Comprehensive playbooks for 4 release scenarios (standard, hotfix, pre-release, recovery)
  • Decision trees for version numbering
  • Templates for changelog management
  • Examples from actual releases

AOT Guru: Optimization Steward

Stewardship:

  • Owns trimming and AOT readiness goals
  • Accountable for binary size targets (5-8 MB minimal)
  • Proactively identifies reflection usage

Learning:

  • Catalogs new AOT incompatibilities discovered
  • Documents workarounds for common patterns
  • Quarterly reviews identify new optimization opportunities
  • Myriad plugin opportunities captured

Review:

  • Quarterly scan of all projects for reflection patterns (IL2026)
  • Monitors binary sizes vs. targets, alerts on creep
  • Reviews generated code (Myriad plugins) for AOT safety
  • Detects new anti-patterns: “We found 5 reflection usages this quarter, suggest Myriad for X pattern”
  • Reports feed automation: “This pattern appears repeatedly, time to create automated detection”

Automation:

  • aot-diagnostics.fsx analyzes projects for reflection
  • aot-analyzer.fsx parses build logs and categorizes IL warnings
  • aot-test-runner.fsx runs multi-platform test matrix
  • Token savings: Automatic analysis instead of manual review

Collaboration:

  • Coordinates with QA Tester on AOT test runs
  • Coordinates with Elm-to-F# Guru on generated code safety
  • Escalates to maintainers for reflection decisions

Teaching:

  • Decision trees: “I have an IL2026 warning. What should I do?”
  • Pattern catalog: Reflection anti-patterns and alternatives
  • Guides: Source generators vs. Myriad vs. manual

QA Tester: Quality Gate

Stewardship:

  • Owns testing standards and coverage
  • Accountable for regression prevention
  • Proactively enforces ≥80% coverage

Learning:

  • Discovers new edge cases in every migration
  • Test failures become regression test additions
  • Coverage trends tracked quarterly

Review:

  • Continuous review of test coverage across all projects
  • Scans for ignored tests or skipped scenarios (why?)
  • Quarterly analysis: Coverage trends, gap patterns, edge cases discovered
  • Reviews BDD scenarios against guidelines: Are they comprehensive? Clear?
  • Identifies testing debt: “We’ve skipped this scenario 3 times, should we fix or remove it?”

Automation:

  • smoke-test.fsx quick sanity check (~2 min)
  • regression-test.fsx full test suite (~10 min)
  • validate-packages.fsx NuGet package verification
  • Savings: Fast feedback loops, high confidence

Collaboration:

  • Works with all gurus on testing their domain
  • Coordinates with Release Manager on pre-release verification
  • Clear standard: ≥80% coverage enforced

Teaching:

  • BDD scenario templates
  • Test plan templates
  • Bug report templates
  • Coverage tracking guide

Building New Gurus

The Guru Creation Checklist

When creating a new guru, embody these principles:

  1. Stewardship

    • Clear domain ownership defined
    • Responsibility boundaries explicit
    • Accountability stated
    • Quality/velocity/responsibility focus clear
  2. Learning

    • Feedback mechanism designed (when/how to capture data)
    • Review schedule established (quarterly? per-session?)
    • Improvement loop designed (feedback → updates → publish)
    • Knowledge base designed (catalog, templates, playbooks)
  3. Review

    • Review scope clearly defined (what issues does this guru look for?)
    • Review triggers established (continuous? scheduled? event-driven?)
    • Review output designed (report format, findings categorization)
    • Review findings fed to: Playbooks, automation, next review criteria
    • Review integrated with retrospectives (findings → prevention → playbook updates)
  4. Automation

    • High-token-cost tasks identified (3-5 candidates)
    • F# scripts created for automation
    • Token savings calculated
    • Automation integrated into workflows
  5. Collaboration

    • Coordination points with other gurus mapped
    • Hand-off protocols designed
    • Escalation paths explicit
    • Error handling at boundaries
  6. Collaboration

    • Coordination points with other gurus mapped
    • Hand-off protocols designed
    • Escalation paths explicit
    • Error handling at boundaries
  7. Teaching

    • Decision trees documented
    • Pattern catalog designed
    • Playbooks written
    • Templates provided

The Guru Creation Phases

graph LR
    subgraph "Phase 1"
        P1[Definition]
    end
    subgraph "Phase 2"
        P2[Implementation]
    end
    subgraph "Phase 3"
        P3[Learning<br/>Integration]
    end
    subgraph "Phase 4"
        P4[Review<br/>Implementation]
    end
    subgraph "Phase 5"
        P5[Collaboration]
    end
    subgraph "Phase 6"
        P6[Teaching]
    end

    P1 --> P2 --> P3 --> P4 --> P5 --> P6

    style P1 fill:#e3f2fd,stroke:#1565c0
    style P2 fill:#e8f5e9,stroke:#2e7d32
    style P3 fill:#fff3e0,stroke:#e65100
    style P4 fill:#fce4ec,stroke:#c2185b
    style P5 fill:#f3e5f5,stroke:#7b1fa2
    style P6 fill:#e0f2f1,stroke:#00695c

Phase 1: Definition

  • Define domain and scope
  • Identify competencies (3-6 primary, 2-4 secondary)
  • Map coordination points
  • Design feedback mechanism

Phase 2: Implementation

  • Write skill.md with comprehensive guidance
  • Create automation scripts (F# for high-token work)
  • Build pattern catalog
  • Design templates

Phase 3: Learning Integration

  • Implement feedback capture
  • Establish review schedule
  • Design playbook evolution
  • Document improvement process

Phase 4: Review Implementation

  • Design review scope and criteria
  • Create review scripts/tooling
  • Establish review schedule and cadence
  • Design integration with playbooks and automation

Phase 5: Collaboration

  • Coordinate with other gurus
  • Test hand-offs
  • Verify escalation paths
  • Validate error handling

Phase 6: Teaching

  • Create decision trees
  • Document patterns
  • Write playbooks
  • Provide templates

Guiding Principles

1. Learn From Every Session

A guru that doesn’t improve is just a prompt.

Every session with a guru should feed insights back into its knowledge system. New patterns, edge cases, failures—all become part of the playbook.

2. Review Proactively

A guru that only reacts to problems is incomplete.

Gurus should scan their domain regularly for issues, guideline violations, and improvement opportunities. Reviews are how gurus stay engaged and make their presence felt. Combine review findings with retrospectives to create continuous improvement loops.

Review ≠ One-Off Code Review:

  • Code review is reactive (“Please review my PR”)
  • Guru review is proactive (“I scanned the project and found these issues”)
  • Code review gives feedback once
  • Guru review captures findings to improve guidance

3. Automate Repetitive Work

Token efficiency is a feature, not an afterthought.

Identify high-token-cost repetitive work and create scripts to automate it. This makes the guru more efficient and the entire project benefit from permanent automation.

4. Document Why, Not Just What

Teaching is as important as doing.

When a guru provides guidance, it should explain the reasoning, not just the answer. This teaches users to make better decisions independently.

5. Collaborate Transparently

Gurus are team members, not black boxes.

Clear hand-offs, explicit coordination, and honest escalation build trust and effectiveness across the guru team.

6. Respect Scope Boundaries

A guru should escalate gracefully when uncertain.

Gurus should know their limits and escalate decisions beyond their scope. This prevents over-confident guidance in unfamiliar territory.

7. Improve Continuously

Quarterly reviews are non-negotiable.

Regular retrospectives, proactive reviews, feedback capture, and playbook updates ensure gurus don’t ossify. A guru that never evolves is essentially deprecated.

The Vision

Imagine a morphir-dotnet project where:

  • Quality is maintained automatically through QA Tester’s standards
  • AOT goals are pursued pragmatically via AOT Guru’s guidance
  • Releases are reliable and predictable thanks to Release Manager’s playbooks
  • Elm-to-F# migration proceeds smoothly with Elm-to-F# Guru’s expertise
  • New domains are stewarded by additional gurus built using proven patterns
  • Every guru improves every quarter through automated feedback
  • Every guru automates high-token work so humans focus on decisions
  • Every guru collaborates gracefully with clear hand-offs
  • Knowledge is preserved and evolved organically through use

This is not a future state. It’s what morphir-dotnet is building now.


Last Updated: December 19, 2025 Philosophy Champion: @DamianReeves Version: 1.0 (Initial Documentation)

16.1.3 - IR Classic Migration and Namespace Strategy

Design guide for Morphir.IR.Classic namespace strategy and migration from morphir-elm

IR Classic Migration and Namespace Strategy

Overview

This document describes the namespace strategy for Morphir IR in the F# implementation, specifically the separation between Morphir.IR.Classic (existing morphir-elm IR) and Morphir.IR (future evolution). This guide serves as a reference for AI agents and human contributors working on the Morphir IR model.

Purpose

The Morphir maintainers recognize that the generic attribute approach in the current IR complicates things, but we need to support existing morphir-elm tools and enable migration of existing code. The namespace strategy allows us to:

  1. Support existing tools: Maintain compatibility with morphir-elm ecosystem
  2. Enable migration: Allow existing morphir-elm code to migrate to F# (and eventually other languages)
  3. Reserve evolution space: Keep Morphir.IR namespace free for future improvements
  4. Document decisions: Provide clear guidance for contributors and AI agents

Namespace Strategy

Morphir.IR.Classic

Purpose: Represents the existing IR available in morphir-elm with generic attributes.

Characteristics:

  • Uses generic attributes (Type<'attributes>, AccessControlled<'T>, etc.)
  • Maintains compatibility with current Morphir ecosystem
  • Supports existing tools and JSON serialization formats
  • Enables migration from morphir-elm codebase

Namespace format: Morphir.IR.Classic (not Morphir.Classic.IR)

Directory structure: IR/Classic/ (not Classic/IR/)

Modules:

  • AccessControlled<'T> - Access control wrapper
  • Type<'attributes> - Type expressions, specifications, and definitions
  • Future: Value<'typeAttributes, 'valueAttributes>, Module, Package, Distribution

Morphir.IR

Purpose: Reserved for future evolution of Morphir IR.

Characteristics:

  • Will support a different, simpler approach to attributes
  • Allows for breaking changes and improvements
  • Future-proof design space
  • Better developer experience

Modules (foundational, no attributes):

  • Name - Human-readable identifiers
  • Path - Hierarchical paths
  • PackageName - Package identifiers
  • ModulePath - Module paths
  • FQName - Fully-qualified names

These foundational modules are building blocks used by both Classic and future IR.

Attribute Approach

Current Classic IR Approach

Generic Attributes: The Classic IR uses generic type parameters for attributes ('attributes, 'T, etc.)

Rationale:

  • Required for compatibility with morphir-elm
  • Enables extensibility (can attach any attribute type)
  • Matches existing morphir-elm implementation

Complications:

  • Adds complexity to type signatures
  • Makes code more verbose
  • Requires generic parameters throughout the type system
  • Can complicate serialization and code generation

Example:

type Type<'attributes> =
    | Variable of 'attributes * Name
    | Reference of 'attributes * FQName * Type<'attributes> list
    // ...

Future IR Approach

Simpler Attributes: The future Morphir.IR namespace will use a simpler attribute approach.

Goals:

  • Less generic, more straightforward
  • Better developer experience
  • Reduced complexity
  • Breaking changes acceptable in Morphir.IR namespace

Note: The exact design of the future attribute approach is still being determined by Morphir maintainers.

Module Organization

Dependency Relationships

Morphir.IR (foundational)
├── Name
├── Path
├── PackageName
├── ModulePath
└── FQName

Morphir.IR.Classic (uses IR modules)
├── AccessControlled<'T>
└── Type<'attributes>
    └── Uses: Name, FQName, AccessControlled

Key Points:

  • Classic modules depend on IR modules (one-way dependency)
  • IR modules are independent and don’t depend on Classic
  • This allows Classic to be optional while IR remains core

Module Placement Guidelines

Morphir.IR (foundational, no attributes):

  • ✅ Name, Path, PackageName, ModulePath, FQName
  • ✅ Any module that doesn’t need attributes
  • ✅ Building blocks used by both Classic and future IR

Morphir.IR.Classic (existing morphir-elm IR with attributes):

  • ✅ Type, AccessControlled
  • ✅ Future: Value, Module, Package, Distribution
  • ✅ Any module that uses generic attributes for morphir-elm compatibility

Migration Path

From morphir-elm to F#

Strategy: Use Morphir.IR.Classic namespace for all modules that match morphir-elm structure.

Steps:

  1. Implement modules in Morphir.IR.Classic namespace
  2. Use generic attributes to match morphir-elm types
  3. Maintain JSON serialization compatibility
  4. Support existing tooling

Example Migration:

-- morphir-elm
type Type attributes
    = Variable attributes Name
    | Reference attributes FQName (List (Type attributes))
// morphir-dotnet (Classic)
namespace Morphir.IR.Classic

type Type<'attributes> =
    | Variable of 'attributes * Name
    | Reference of 'attributes * FQName * Type<'attributes> list

From Classic to Future IR

Strategy: When future IR is designed, new modules will go in Morphir.IR namespace.

Considerations:

  • Breaking changes are acceptable in Morphir.IR
  • Classic modules remain for backward compatibility
  • Migration tools may be provided to convert Classic → IR
  • Both namespaces can coexist

Design Decisions

Why Generic Attributes in Classic?

  1. Compatibility: Required to match morphir-elm implementation
  2. Tool Support: Existing tools expect generic attributes
  3. Migration: Enables direct translation from morphir-elm
  4. Extensibility: Allows attaching various attribute types

Why Separate Namespaces?

  1. Evolution Space: Reserves Morphir.IR for future improvements
  2. Clear Separation: Makes it obvious which modules are “classic” vs “future”
  3. Backward Compatibility: Classic modules remain available
  4. Gradual Migration: Allows incremental adoption of future IR

Trade-offs and Considerations

Pros:

  • Supports existing ecosystem
  • Enables migration from morphir-elm
  • Clear separation of concerns
  • Future-proof design

Cons:

  • Generic attributes add complexity
  • Two namespaces to understand
  • Potential confusion about which to use
  • Maintenance of both namespaces

Future Evolution

Plans for Morphir.IR Namespace

The Morphir maintainers plan to evolve the IR with a simpler attribute approach. The Morphir.IR namespace is reserved for this evolution.

Timeline: TBD by Morphir maintainers

Breaking Changes: Acceptable in Morphir.IR namespace

Backward Compatibility: Classic modules will remain available

Migration Strategy

When future IR is ready:

  1. New modules go in Morphir.IR
  2. Classic modules remain for compatibility
  3. Migration tools may be provided
  4. Documentation will guide users

Directory Structure

The directory structure matches the namespace:

src/Morphir.Models/
  IR/
    Name.fs                    → namespace Morphir.IR
    Path.fs                    → namespace Morphir.IR
    PackageName.fs             → namespace Morphir.IR
    ModulePath.fs              → namespace Morphir.IR
    FQName.fs                  → namespace Morphir.IR
    Classic/
      AccessControlled.fs      → namespace Morphir.IR.Classic
      Type.fs                  → namespace Morphir.IR.Classic

Key Point: IR/Classic/ directory structure matches Morphir.IR.Classic namespace.

Reference for AI Agents

When implementing IR modules:

  1. Check namespace: Is this a foundational module (no attributes) or Classic module (with attributes)?
  2. Use appropriate namespace:
    • Foundational → Morphir.IR
    • Classic → Morphir.IR.Classic
  3. Follow directory structure: Match namespace with directory structure
  4. Maintain dependencies: Classic can depend on IR, but not vice versa
  5. Document decisions: Add notes about why modules are in their chosen namespace

Summary

  • Morphir.IR.Classic: Existing morphir-elm IR with generic attributes
  • Morphir.IR: Future evolution with simpler attributes
  • Directory: IR/Classic/ matches Morphir.IR.Classic namespace
  • Strategy: Support existing tools while reserving space for future improvements
  • Migration: Enable morphir-elm → F# migration while planning for future evolution

16.1.4 - Guru Creation Guide

Step-by-step guide for creating new AI gurus in morphir-dotnet

Guru Creation Guide

Overview

This guide walks through the process of creating a new guru (AI skill) in morphir-dotnet. It establishes a repeatable pattern that ensures consistency, quality, and alignment with the guru philosophy.

A guru should be created when you have a domain of expertise that:

  • Is distinct and has clear boundaries
  • Crosses multiple project areas or is deep within one area
  • Has 3+ core competencies (expertise areas)
  • Contains repetitive work suitable for automation

Part 1: Should This Be a Guru?

Decision Framework

Use this flowchart to determine if you should create a guru:

flowchart TD
    START([Start]) --> Q1{Is it a<br/>distinct domain?}
    Q1 -->|No| ALT1[Create .agents/ guide<br/>or AGENTS.md section]
    Q1 -->|Yes| Q2{Does it justify<br/>deep expertise?<br/>20+ patterns?}
    Q2 -->|No| ALT2[Document in<br/>AGENTS.md]
    Q2 -->|Yes| Q3{3+ core<br/>competencies?}
    Q3 -->|No| ALT3[Add to existing guru<br/>or create guide]
    Q3 -->|Yes| Q4{High-token-cost<br/>repetitive work?}
    Q4 -->|No| ALT4[Create .agents/ guide<br/>No automation needed]
    Q4 -->|Yes| Q5{Will coordinate<br/>with other gurus?}
    Q5 -->|No| ALT5[Standalone skill<br/>or utility]
    Q5 -->|Yes| CREATE([Create a Guru!])

    style START fill:#e8f5e9,stroke:#2e7d32
    style CREATE fill:#c8e6c9,stroke:#2e7d32
    style ALT1 fill:#fff3e0,stroke:#e65100
    style ALT2 fill:#fff3e0,stroke:#e65100
    style ALT3 fill:#fff3e0,stroke:#e65100
    style ALT4 fill:#fff3e0,stroke:#e65100
    style ALT5 fill:#fff3e0,stroke:#e65100

Ask yourself these questions in order:

1. Is it a distinct domain?

  • Question: Can I clearly define what this guru owns?
  • Examples: Yes
    • Testing/QA (QA Tester)
    • AOT optimization (AOT Guru)
    • Release management (Release Manager)
    • Elm-to-F# migration (Elm-to-F# Guru)
  • Examples: No
    • “Helping with random coding tasks” (too broad)
    • “One-off problem solving” (not a domain)

2. Does it justify deep expertise?

  • Question: Is there enough depth to warrant 20+ patterns and multiple playbooks?
  • Examples: Yes
    • QA has 20+ testing patterns (unit, BDD, E2E, property-based, etc.)
    • AOT has 15+ IL error categories and workarounds
    • Release has 4 playbooks (standard, hotfix, pre-release, recovery)
  • Examples: No
    • One-off task (create a simple script instead)
    • Straightforward process (document in AGENTS.md instead)

3. Will it have 3+ core competencies?

  • Question: Can I identify at least 3 areas of expertise?
  • Examples: Yes
    • QA: Test planning, automation, coverage tracking, bug reporting, BDD design
    • AOT: Diagnostics, size optimization, source generators, Myriad, IL parsing
    • Release: Version management, changelog handling, deployment monitoring, recovery
  • Examples: No
    • Only 1-2 areas (document as section of existing guru or create guide)

4. Is there high-token-cost repetitive work?

  • Question: Will automation save significant tokens or effort?
  • Examples: Yes
    • Release: Monitoring workflow status manually (tokens) → autonomous polling (few tokens)
    • AOT: Reading IL warnings manually (tokens) → automated analysis (few tokens)
    • QA: Running tests manually (tokens) → automated test runner (few tokens)
  • Examples: No
    • Guidance only (no scripts needed, create .agents/ guide instead)
    • One-off automation (create a utility script, not a guru)

5. Will it coordinate with other gurus?

  • Question: Does this domain have clear integration points?
  • Examples: Yes
    • Elm-to-F# → AOT Guru (verify generated code is AOT-compatible)
    • Elm-to-F# → QA Tester (verify test coverage)
    • Release Manager ↔ QA Tester (post-release verification)
  • Examples: No
    • Isolated domain (could be .agents/ guide or standalone skill)

Decision Result

If all 5 questions are YES → Create a guru skill

If any are NO → Consider alternatives:

  • Just 1-2 competencies? → Create .agents/{topic}.md guide instead
  • No automation opportunity? → Document decision trees in AGENTS.md
  • No coordination needed? → Create standalone utility or guide
  • Too narrow/specific? → Create template or plugin, not full guru

Part 2: Guru Definition

Step 1: Define the Domain

Write a clear 2-3 sentence description:

Domain: Release Management
Description: Orchestrating the complete release lifecycle from version planning 
through deployment and verification. Ensures releases are reliable, predictable, 
and recoverable.

Step 2: Define Competencies

List primary and secondary competencies:

Primary Competencies (3-6 core areas):

  1. Version Management - Semantic versioning, version detection
  2. Changelog Management - Keep a Changelog format, parsing, generation
  3. Deployment Orchestration - Workflow automation, status tracking
  4. Verification & Recovery - Post-release checks, failure recovery
  5. Process Improvement - Retrospectives, playbook evolution
  6. Documentation - Comprehensive playbooks, decision trees

Secondary Competencies (2-4 supporting areas):

  1. Git/GitHub Coordination - Tag management, branch strategies
  2. CI/CD Integration - GitHub Actions, workflow triggers
  3. Communication - Status updates, failure alerts
  4. Historical Analysis - Release metrics, trend tracking

Step 3: Define Responsibilities

What is this guru accountable for?

Release Manager Responsibilities:
- Ensure releases happen on schedule without surprises
- Prevent release failures through pre-flight validation
- Enable fast recovery if failures occur
- Improve the release process continuously (quarterly reviews)
- Communicate clearly about status and blockers
- Coordinate with QA on verification and AOT Guru on version compatibility

Step 4: Define Scope Boundaries

What is explicitly NOT this guru’s responsibility?

Release Manager Does NOT:
- Make product decisions about what features to include
- Review code quality (that's QA Tester's job)
- Decide version numbering policies (that's maintainers' decision)
- Handle security issues (that's future Security Guru's job)
- Manage documentation (that's future Documentation Guru's job)

Step 5: Map Coordination Points

Identify other gurus this will coordinate with:

Release Manager Coordination:
- WITH QA Tester: Hand-off after release for verification
  - Trigger: Release deployed
  - Signal: "Ready for post-release QA?"
  - Response: Test results, coverage, functional verification
  
- WITH AOT Guru: Verify version tags are AOT-compatible
  - Trigger: Before publishing release
  - Signal: "Can I publish this version?"
  - Response: AOT status, any breaking changes
  
- WITH Elm-to-F# Guru: Track feature parity milestones
  - Trigger: Migration progress updates
  - Signal: "What's our migration status for this release?"
  - Response: Completed modules, parity progress

Part 3: Implementation Structure

Directory Layout

Create the following structure:

graph TB
    subgraph ".claude/skills/{guru-name}/"
        SKILL["📄 skill.md<br/>Main skill prompt<br/>1000-1200 lines"]
        README["📄 README.md<br/>Quick reference<br/>300-400 lines"]
        MAINT["📄 MAINTENANCE.md<br/>Review process"]

        subgraph "scripts/"
            S1["automation-1.fsx"]
            S2["automation-2.fsx"]
            S3["common.fsx"]
        end

        subgraph "templates/"
            T1["decision-template.md"]
            T2["workflow-template.md"]
        end

        subgraph "patterns/"
            P1["pattern-1.md"]
            P2["pattern-2.md"]
            P3["...grows over time"]
        end
    end

    style SKILL fill:#fff3e0,stroke:#e65100
    style README fill:#e8f5e9,stroke:#2e7d32
    style MAINT fill:#e3f2fd,stroke:#1565c0
.claude/skills/{guru-name}/
├── skill.md                    # Main skill prompt (1000-1200 lines)
├── README.md                   # Quick start guide (300-400 lines)
├── MAINTENANCE.md              # Quarterly review process
├── scripts/
│   ├── automation-1.fsx        # High-token-cost task automation
│   ├── automation-2.fsx        # High-token-cost task automation
│   ├── automation-3.fsx        # High-token-cost task automation
│   └── common.fsx              # Shared utilities
├── templates/
│   ├── {decision-type}-decision.md
│   ├── {workflow-type}-workflow.md
│   └── issue-template.md
└── patterns/
    ├── pattern-1.md
    ├── pattern-2.md
    └── [more patterns discovered over time]

skill.md Structure

Your main skill file should contain:

---
id: {guru-id}
name: {Guru Name}
triggers:
  - keyword1
  - keyword2
  - keyword3
---

# {Guru Name}

## Overview
[2-3 sentences about the guru]

## Responsibilities
[List of core responsibilities]

## Competencies
[Detailed list of competencies with examples]

## Decision Trees
[3-5 decision trees for common scenarios]

## Playbooks
[3-5 detailed workflows]

## Pattern Catalog
[Growing collection of patterns]

## Automation
[Available F# scripts]

## Integration Points
[How this guru coordinates with others]

## Feedback Loop
[How this guru improves over time]

## Related Resources
[Links to guides, documentation, templates]

Size Target: 1000-1200 lines (~50 KB)

README.md Structure

Quick reference for users:

# {Guru Name} - Quick Reference

## What This Guru Does
[One paragraph overview]

## When to Use This Guru
[List of scenarios]

## Core Competencies
[Quick bullet list]

## Available Scripts
[Table of scripts with descriptions]

## Common Tasks
[Quick how-tos]

## Pattern Catalog
[Index of patterns]

## Examples
[Real examples from the project]

## Integration
[How to use this guru with others]

## References
[Links to related documentation]

Size Target: 300-400 lines (~16 KB)

MAINTENANCE.md Structure

Guidance for maintaining this guru:

# Maintenance Guide

## Quarterly Review Checklist
- [ ] Read through collected feedback
- [ ] Identify 2-3 improvements for next quarter
- [ ] Update patterns that changed
- [ ] Create/update Myriad plugins if automation opportunities exist
- [ ] Document learnings in Implementation Notes
- [ ] Update success metrics

## Feedback Collection
- Where feedback is captured: [GitHub issue, tracking doc, etc.]
- Review schedule: [Quarterly, per-release, etc.]
- Stakeholders to consult: [maintainers, project leads]

## Improvement Process
1. Collect feedback
2. Identify patterns
3. Update playbooks/templates
4. Test changes
5. Document in changelog
6. Publish update

## Version History
[Track skill evolution]

F# Scripts

Create 3-5 scripts targeting high-token-cost tasks:

Script Template:

#!/usr/bin/env -S dotnet fsi

/// Automation Script: {Purpose}
/// Saves {N} tokens per use by automating {high-token-cost task}
/// Usage: dotnet fsi {script-name}.fsx [args]

#r "nuget: Spectre.Console"
open Spectre.Console

let main argv =
    // Parse arguments
    // Analyze/test/validate something
    // Print results
    0

exit (main fsx.CommandLineArgs.[1..])

Script checklist:

  • Clear purpose stated in comments
  • Token savings estimated
  • Usage documented
  • Error handling included
  • JSON output option (for automation)
  • Progress indicators (for long-running scripts)

Templates

Create domain-specific templates:

Decision Template:

# Decision: {Decision Type}

## Scenario
[When would you make this decision?]

## Options
1. Option A
   - Pros: ...
   - Cons: ...
   - When to use: ...

2. Option B
   - Pros: ...
   - Cons: ...
   - When to use: ...

## Recommendation
[What does the guru recommend?]

## Examples
[Real examples from the project]

Workflow Template:

# Workflow: {Workflow Name}

## Overview
[What does this workflow accomplish?]

## Prerequisites
[What must be true before starting?]

## Steps
1. Step 1 - [description]
2. Step 2 - [description]
...

## Validation
[How do you know it worked?]

## Rollback
[How do you undo if it fails?]

## Related Workflows
[Links to similar workflows]

Pattern Catalog

Start with 5-10 seed patterns, add more as discovered:

Pattern Entry Template:

# Pattern: {Pattern Name}

## Description
[What is this pattern?]

## Context
[When and why would you use it?]

## Examples
[Real code examples]

## Pros and Cons
[Trade-offs]

## Related Patterns
[Similar or complementary patterns]

## References
[Links to documentation or standards]

Part 4: Automation Strategy

Identify High-Token-Cost Tasks

For your guru domain, identify 5-10 repetitive tasks:

Release Manager Example:

  1. Check GitHub Actions status (manual every 5 min = many tokens)
  2. Prepare release checklist (manual validation = tokens)
  3. Validate post-release status (manual testing = tokens)
  4. Extract release history for notes (manual searching = tokens)
  5. Detect process changes (manual review = tokens)

Prioritize for Automation

Score tasks on:

  • Frequency: How often does this happen? (1-5 scale)
  • Token Cost: How many tokens does it cost? (1-5 scale)
  • Repetitiveness: Is this the same every time? (1-5 scale)
Task                          Frequency  Token Cost  Repetitive  Priority
Monitor release status        5 (every few min)  3       5        Critical
Prepare checklist             3 (per release)    2       5        High
Post-release validation       3 (per release)    3       5        High
Extract release history       2 (per release)    2       3        Medium
Detect process changes        1 (quarterly)      2       4        Medium

Select top 3-5 for automation

Design Automation Scripts

For each task, design an F# script:

Script Design Pattern:

  1. Input: What data does this need?
  2. Processing: What analysis/transformation?
  3. Output: What does it return?
  4. Token Savings: How much does this save?

Example: Monitor Release Status

Input: GitHub Action workflow ID
Processing: Poll GitHub Actions API, track status
Output: Current status, elapsed time, next check
Token Savings: 100+ tokens per hour (vs. manual polling)

Part 5: Feedback Mechanisms

Design Feedback Capture

Define when and how the guru learns:

Trigger Points:

  • After workflow completion? (success/failure)
  • After N sessions? (every 5 migrations)
  • On quarterly schedule? (Q1, Q2, Q3, Q4)
  • After escalations? (decisions beyond scope)

Capture Methods:

  • GitHub tracking issue (Release Manager model)
  • IMPLEMENTATION.md notes (AOT Guru model)
  • Automated prompts in skill (your choice)
  • Quarterly review meetings (maintainer involvement)

Example: Elm-to-F# Guru

Capture Trigger: After each module migration
Capture Method: Migration template includes "Patterns Discovered" section
Review Schedule: Quarterly pattern inventory review
Improvement Action: If pattern appears 3+ times, create Myriad plugin

Q1: Discovered 15 new patterns
Q2: Created 2 Myriad plugins for repetitive patterns
Q3: Updated decision trees based on learnings
Q4: Plan next quarter's automation

Design Review Process

Define quarterly reviews:

  1. Collect: Gather all feedback from past quarter
  2. Analyze: Identify 2-3 key improvements
  3. Decide: What will change? What won’t?
  4. Update: Modify playbooks, templates, patterns
  5. Document: Record what changed and why
  6. Communicate: Let users know about improvements

Review Checklist:

  • Feedback reviewed (N items)
  • Improvement areas identified (3-5 topics)
  • Playbooks updated (X changes)
  • Patterns added/modified (Y patterns)
  • Automation opportunities identified (Z scripts to create)
  • Version bumped if user-facing changes

Part 5B: Review Capability

Design the Review Scope

A guru should proactively review its domain for issues, guideline violations, and improvement opportunities.

Define Review Scope Questions:

  1. What issues should this guru look for?

    • AOT Guru: Reflection usage, binary size creep, trimming-unfriendly patterns
    • QA Tester: Coverage gaps, ignored tests, missing edge cases, guideline violations
    • Release Manager: Process deviations, changelog quality, version inconsistencies
    • Elm-to-F# Guru: Migration anti-patterns, Myriad plugin opportunities, F# idiom violations
  2. How often should reviews run?

    • Continuous (real-time detection)
    • Per-session (after each major workflow)
    • Weekly (scheduled scan)
    • Quarterly (comprehensive review)
    • On-demand (user-triggered)
  3. What triggers a review?

    • Code push? (CI/CD trigger)
    • Release? (post-release verification)
    • Schedule? (weekly, quarterly)
    • Escalation? (manual request)
  4. What’s the output format?

    • Report document (Markdown table of findings)
    • GitHub issues (one issue per finding)
    • Notification (Slack, PR comment)
    • Integrated summary (skill guidance update)
  5. How do review findings feed back?

    • To playbooks: “We found 3 reflection patterns, add to decision tree”
    • To automation: “This pattern appears repeatedly, create detection script”
    • To retrospectives: “Q1 findings suggest process changes”
    • To next review criteria: “Focus on this area going forward”

Create Review Scripts

Design and implement F# scripts that perform reviews:

Example: AOT Guru’s Quarterly Review

// scripts/aot-scan.fsx - Quarterly review of all projects
// Scans for:
//   - Reflection usage (IL2026 patterns)
//   - Binary sizes vs. targets
//   - Trimming-unfriendly patterns (static fields, etc.)
//
// Output: Markdown report with findings, trends, recommendations

Findings:
- Reflection in 7 locations (5 in serialization, 2 in codegen)
- Binary sizes: 8.2 MB (target 8 MB) - creeping by ~200 KB/quarter
- New pattern: ValueTuple boxing in LINQ chains (appears 3x)
- Opportunities: 2 patterns ready for Myriad plugin automation

Recommendations:
- Create aot-serializer.fsx (Myriad plugin) for serialization reflection
- Add ValueTuple boxing detection to aot-diagnostics.fsx
- Set size limit at 8.5 MB (buffer) or refactor

Next Quarter Focus:
- Monitor ValueTuple pattern frequency
- Implement Myriad plugin if pattern appears >5 more times
- Evaluate serialization library alternatives

Integrate Review with Retrospectives

Design how reviews and retrospectives work together:

Review (Proactive):
  "I scanned the code and found these issues"
  └─ Findings feed into retrospectives

Retrospective (Reactive):
  "That failure happened because of X"
  └─ Root cause feeds into reviews: "Start looking for X pattern"

Together: Continuous improvement cycle
  Findings → Prevention → Process update → Review criteria → Next quarter

Example Integration:

Q1 Review Findings:
- "We found 5 ignored tests. Why?"

Q1 Retrospective:
- "Test X was failing intermittently. We skipped it to unblock releases."

Q1 Outcomes:
- Fix root cause of flaky test
- Add test to monitoring criteria
- Playbook update: "Always investigate skipped tests in Q1 review"

Q2 Review:
- Monitors for skipped tests automatically
- Finds 0 skipped tests (improvement!)
- Pattern: "Skipped tests went from 5 → 0"

Design Review Integration Points

Define where reviews fit in the workflow:

Option A: Continuous Review

  • Trigger: Every code push to main
  • Runs: During CI/CD
  • Output: GitHub check or PR comment
  • Effort: Medium (depends on scan speed)

Option B: Scheduled Review

  • Trigger: Weekly or quarterly
  • Runs: Off-hours or on-demand
  • Output: Report + GitHub issues for findings
  • Effort: Low (scheduled, low impact)

Option C: Session-Based Review

  • Trigger: After each major workflow (migration, release)
  • Runs: As part of workflow
  • Output: Integrated into workflow results
  • Effort: Varies (per-session analysis)

Option D: Manual Review

  • Trigger: User request ("@guru review")
  • Runs: On-demand
  • Output: Full report generated immediately
  • Effort: Medium (real-time analysis)

Review Checklist

When implementing review capability:

  • Review scope clearly defined (what issues to look for)
  • Review trigger designed (when does review run)
  • Review scripts created (F# implementation)
  • Review output format chosen (report/issues/notification)
  • Review findings documented (findings structure)
  • Integration with retrospectives designed
  • Integration with automation strategy designed
  • Integration with playbooks designed
  • Review schedule established (continuous/weekly/quarterly/on-demand)
  • Tested on real project data (not just examples)

Part 6: Cross-Agent Compatibility

Ensure Scripts Work Everywhere

Your F# scripts should work for Claude Code, Copilot, and all other agents:

Checklist:

  • Scripts use standard F# (no Claude-specific features)
  • Scripts have clear usage documentation
  • Scripts produce JSON output option (for parsing)
  • Scripts have exit codes (0 = success, 1 = validation failure, 2 = error)
  • Scripts document dependencies (required NuGet packages)
  • Scripts work on Windows, Mac, Linux

Document for All Agents

Your README and documentation should explain:

For Claude Code users:

  • How to invoke via @skill syntax
  • What YAML triggers work

For Copilot users:

  • How to read .agents/ equivalent guides
  • How to run scripts directly

For other agents:

  • How to find and copy this skill’s README
  • How to run scripts directly

Example section:

## Using This Guru

**Claude Code:** Mention keywords like "release", "deploy", "publish"
**Copilot:** Read .agents/release-manager.md for equivalent guidance
**Other agents:** Run scripts directly: `dotnet fsi scripts/monitor-release.fsx`

Cross-Project Portability

Document how this guru could be used in other projects:

## Using This Guru in Other Projects

### Portable Components
- Decision trees (universal for this domain)
- Pattern catalog (concepts apply broadly)
- Script utilities (adapt paths for new project)

### Non-Portable Components
- Project-specific playbooks (morphir-dotnet release process)
- Integration with NUKE build system
- Version numbering conventions

### To Adapt to New Project
1. Update script paths (if paths differ)
2. Update build system integration (if not NUKE)
3. Adapt playbooks to new project's process
4. Customize templates for new project conventions

Estimated effort: 4-8 hours

Part 7: Workflow & Validation

Red-Green-Refactor for Skill Development

Follow TDD principles even for skills:

Red: Write test scenarios for the skill

  • Create BDD features showing how the guru should behave
  • Create decision tree tests (“Given this scenario, recommend this”)

Green: Implement skill.md

  • Write guidance that makes tests pass
  • Create playbooks covering test scenarios

Refactor: Improve skill based on feedback

  • Test with real scenarios
  • Get feedback from team
  • Update guidance and playbooks

BDD Scenarios for Skills

Create .feature files demonstrating skill behavior:

Feature: Release Manager Guru

  Scenario: Release fails and guru captures retrospective
    Given a release is in progress
    When the release fails
    Then the guru should prompt for "What went wrong?"
    And capture the response in the tracking issue
    And suggest prevention strategies

  Scenario: After 3 successful releases, guru prompts for improvements
    Given 3 consecutive successful releases
    When starting the 4th release
    Then the guru should ask "What could we improve?"

Testing Checklist

Before releasing your guru:

  • Read through skill.md (is it clear? comprehensive?)
  • Test all automation scripts (do they work? return correct output?)
  • Validate decision trees (do they handle real scenarios?)
  • Check playbooks (are they complete? any steps missing?)
  • Review templates (are they usable? any clarifications needed?)
  • Test cross-agent compatibility (can Copilot users find equivalent info?)
  • Verify coordination (do other gurus know about this one?)
  • Get team feedback (does this feel useful? any blind spots?)

Part 8: Success Criteria

For Skill Delivery

  • Directory structure created
  • skill.md written (1000+ lines)
  • README.md created (300-400 lines)
  • MAINTENANCE.md documented
  • 3-5 automation scripts implemented
  • 5-10 seed patterns documented
  • 3-5 templates created
  • Coordination points identified
  • Cross-agent compatibility verified
  • Team feedback incorporated

For Skill Maturity (After First Quarter)

  • Feedback capture mechanism working
  • Quarterly review completed
  • 15+ patterns in catalog
  • 3+ improvements made based on feedback
  • 1+ new automation scripts created (if opportunities found)
  • Playbooks updated with learnings
  • Documentation updated
  • Version bumped (if user-facing changes)
  • Success metrics documented

For Skill Excellence (After Two Quarters)

  • 20+ patterns in catalog
  • 2+ custom Myriad plugins (if applicable)
  • Automated feedback mechanism working smoothly
  • Token efficiency analysis complete
  • Cross-project reuse strategy documented
  • Integration with other gurus proven
  • Continuous improvement cycle established
  • Learning system generating insights

Checklist: Creating a New Guru

The guru creation process follows these phases:

graph LR
    subgraph "Planning"
        P1[Define Domain]
        P2[Map Competencies]
        P3[Design Feedback]
    end

    subgraph "Implementation"
        I1[Create Structure]
        I2[Write skill.md]
        I3[Build Scripts]
    end

    subgraph "Validation"
        V1[Test Scripts]
        V2[Verify Trees]
        V3[Get Feedback]
    end

    subgraph "Launch"
        L1[Update AGENTS.md]
        L2[Announce]
        L3[Capture Learning]
    end

    subgraph "Evolution"
        E1[Quarterly Review]
        E2[Update Patterns]
        E3[Improve]
    end

    Planning --> Implementation --> Validation --> Launch --> Evolution
    Evolution -.->|Continuous| Evolution

    style P1 fill:#e3f2fd
    style I1 fill:#e8f5e9
    style V1 fill:#fff3e0
    style L1 fill:#fce4ec
    style E1 fill:#f3e5f5

Use this checklist when creating a new guru:

Planning Phase

  • Domain clearly defined
  • 3+ competencies identified
  • Responsibilities documented
  • Scope boundaries explicit
  • Coordination points mapped
  • Feedback mechanism designed
  • Review schedule established

Implementation Phase

  • Directory structure created
  • skill.md written (1000+ lines)
  • README.md written (300-400 lines)
  • MAINTENANCE.md created
  • 3-5 automation scripts (high-token-cost tasks)
  • 5-10 seed patterns
  • 3-5 templates
  • Examples from real project

Validation Phase

  • Skill.md reviewed for clarity
  • Scripts tested (all work?)
  • Decision trees validated (real scenarios)
  • Playbooks verified (complete steps)
  • Templates usable (examples included)
  • Team feedback collected
  • Cross-agent compatibility checked
  • Coordination with other gurus verified

Launch Phase

  • Referenced in AGENTS.md
  • Added to .agents/skills-reference.md
  • Announcement to team
  • Integration guide created
  • First feedback collected
  • Initial learnings captured

Evolution Phase (After 1 Quarter)

  • Quarterly review completed
  • Feedback analyzed
  • 2-3 improvements made
  • Documentation updated
  • Version bumped
  • Team notified of improvements
  • Next quarter’s improvements planned

Last Updated: December 19, 2025 Created By: @DamianReeves Version: 1.0 (Initial Release)

16.1.5 - Technical Writer Skill Requirements

Requirements for the Technical Writer skill (guru) for documentation and visual communication

Technical Writer Skill - Requirements Document

Status: Draft Version: 0.2.0 Created: 2025-12-19 Updated: 2025-12-19 Author: @DamianReeves

Executive Summary

This document defines the requirements for a new Technical Writer skill (guru) for the morphir-dotnet project. The Technical Writer is more than a documentation maintainer—they are a communication craftsperson who transforms complex technical concepts into clear, engaging, and visually compelling documentation.

The Technical Writer skill combines expertise in:

  • Content Creation: Technical writing, documentation structure, style consistency
  • Visual Communication: Mermaid diagrams, PlantUML, visual storytelling
  • Documentation Infrastructure: Hugo static site generator, Docsy theme mastery
  • Brand Identity: Consistent voice, tone, and visual identity across all documentation

This skill ensures that Morphir .NET has a consistent, well-crafted identity that makes complex concepts accessible and helps users succeed.


Part 1: Should This Be a Guru?

Decision Framework Validation

QuestionAnswerJustification
1. Is it a distinct domain?YESTechnical writing, visual communication, Hugo/Docsy expertise, documentation structure, and content governance are distinct from coding, testing, AOT optimization, and release management
2. Does it justify deep expertise?YES30+ patterns possible: API documentation, tutorials, ADRs, code examples, README structure, changelog format, What’s New documents, troubleshooting guides, Mermaid diagrams, PlantUML architecture diagrams, Hugo shortcodes, Docsy customization, visual storytelling, etc.
3. Will it have 3+ core competencies?YES9 core competencies: Documentation strategy, Hugo/Docsy mastery, visual communication (Mermaid/PlantUML), API documentation, example code management, style guide enforcement, brand identity, markdown mastery, content governance
4. Is there high-token-cost repetitive work?YESLink validation, example code freshness checking, documentation coverage analysis, style consistency checking, diagram validation, Hugo build troubleshooting, Docsy theme configuration
5. Will it coordinate with other gurus?YESRelease Manager (release notes, What’s New), QA Tester (test documentation, BDD scenarios), AOT Guru (AOT/trimming guide maintenance), all gurus (consistent visual identity and communication patterns)

Result: All 5 questions are YES - proceed with guru creation.


Part 2: Domain Definition

Domain Description

Domain: Technical Documentation, Visual Communication, and Documentation Infrastructure

Description: Expert communication craftsperson for morphir-dotnet who transforms complex technical concepts into clear, engaging, and visually compelling documentation. Masters the complete documentation stack from content creation through Hugo/Docsy infrastructure. Ensures Morphir .NET has a consistent, well-crafted identity that fosters understanding and helps users succeed.

The Technical Writer is the go-to team member for:

  • Solving communication challenges through writing
  • Making Hugo and Docsy comply with project needs
  • Creating diagrams and visuals that make concepts pop
  • Applying patterns and templates from successful documentation sites
  • Maintaining consistent brand identity across all documentation

Primary Competencies (9 Core Areas)

  1. Documentation Strategy & Architecture

    • Design documentation structure and navigation
    • Define content types and their purposes
    • Establish documentation hierarchy
    • Plan documentation roadmap aligned with features
    • Analyze successful documentation sites for applicable patterns
  2. Hugo & Static Site Expertise

    • Master of Hugo static site generator configuration
    • Expert troubleshooter for Hugo build issues
    • Deep understanding of Hugo templating and shortcodes
    • Content organization using Hugo sections and taxonomies
    • Hugo modules and dependency management
    • Performance optimization for documentation sites
  3. Docsy Theme Mastery

    • Complete understanding of Docsy theme architecture
    • Customization of Docsy components and layouts
    • Navigation configuration and sidebar management
    • Search configuration (offline and online)
    • Feedback widgets and user engagement features
    • Version switcher and multi-version documentation
    • Responsive design and mobile optimization
  4. Visual Communication & Diagramming

    • Mermaid Mastery: Flowcharts, sequence diagrams, class diagrams, state diagrams, entity relationship diagrams, Gantt charts, pie charts, journey maps
    • PlantUML Expertise: Architecture diagrams, component diagrams, deployment diagrams, detailed UML
    • Visual storytelling that makes complex concepts accessible
    • Consistent diagram styling and branding
    • Integration of diagrams into Hugo/Docsy
    • Decision trees for choosing the right diagram type
  5. Markdown Mastery

    • Expert-level markdown authoring
    • GitHub-flavored markdown extensions
    • Hugo-specific markdown features
    • Tables, code blocks, callouts, admonitions
    • Accessible markdown patterns
    • Markdown linting and consistency
  6. API Documentation

    • Generate and maintain XML doc comments
    • Create API reference documentation
    • Document public interfaces, types, and methods
    • Ensure documentation coverage for public APIs
    • Integrate API docs with Hugo site
  7. Tutorial & Guide Creation

    • Write getting started guides that actually work
    • Create step-by-step tutorials with clear progression
    • Develop conceptual explanations with visual aids
    • Build learning paths for different audiences
    • Ensure tutorials are tested and maintained
  8. Brand Identity & Style Guide

    • Define and enforce documentation style standards
    • Maintain consistent voice and tone
    • Establish visual identity (colors, icons, diagrams)
    • Terminology glossary and naming conventions
    • Ensure accessibility compliance (WCAG)
    • Create templates that embody brand identity
  9. Content Governance

    • Track documentation freshness
    • Manage documentation debt
    • Handle documentation deprecation
    • Coordinate documentation reviews
    • Quality assurance across all documentation types
    • Continuous improvement based on user feedback

Secondary Competencies (5 Supporting Areas)

  1. Cross-Reference Management

    • Maintain internal links using Hugo ref/relref
    • Validate external references
    • Update links when content moves
    • Generate link reports
    • Implement breadcrumbs and navigation aids
  2. Search & Discoverability

    • Optimize content for searchability
    • Configure Hugo/Docsy search features
    • Maintain metadata, tags, and taxonomies
    • Structure content for navigation
    • SEO for documentation sites
  3. Example Code Management

    • Create and maintain code examples
    • Ensure examples compile and run
    • Keep examples synchronized with API changes
    • Test examples in CI/CD pipeline
    • Use Hugo shortcodes for code highlighting
  4. Localization Readiness

    • Prepare content for translation
    • Hugo i18n configuration
    • Use translatable patterns
    • Avoid culture-specific idioms
    • Maintain terminology glossary
  5. Documentation CI/CD

    • Hugo build pipeline configuration
    • Preview deployments for PRs
    • Automated link checking
    • Example code validation
    • Documentation site deployment

Responsibilities

The Technical Writer skill is accountable for:

  • Documentation Quality: Ensure all documentation is accurate, clear, and compelling
  • Visual Excellence: Create diagrams and visuals that make complex concepts accessible
  • Brand Consistency: Maintain Morphir’s identity across all documentation
  • Hugo/Docsy Health: Keep the documentation site building and rendering correctly
  • Coverage: Maintain documentation coverage targets (public APIs, features, workflows)
  • Freshness: Keep documentation synchronized with code changes
  • Discoverability: Make documentation easy to find and navigate
  • Examples: Ensure code examples are working and up-to-date
  • Cross-references: Maintain valid internal and external links
  • Problem Solving: Be the go-to resource for documentation and communication challenges
  • Pattern Application: Research and apply successful patterns from other projects
  • Coordination: Work with other skills on documentation needs

Scope Boundaries (What Technical Writer Does NOT Do)

The Technical Writer skill does NOT:

  • Make product decisions about features (that’s maintainers’ decision)
  • Write or review code implementation (that’s Development agents’ job)
  • Perform QA testing beyond documentation verification (that’s QA Tester’s job)
  • Handle release orchestration (that’s Release Manager’s job)
  • Optimize code for AOT/trimming (that’s AOT Guru’s job)
  • Make architectural decisions (that’s maintainers and architects’ decision)
  • Handle security assessments (that’s future Security Guru’s job)

Coordination Points

Technical Writer Coordination:

- WITH Release Manager:
  Trigger: Release preparation begins
  Signal: "What documentation needs updating for this release?"
  Response: What's New document, release notes, changelog review
  Hand-off: Documentation ready for release publication

- WITH QA Tester:
  Trigger: New feature or PR completion
  Signal: "Does documentation match tested behavior?"
  Response: Documentation accuracy verification
  Hand-off: Documentation verified against test results

- WITH AOT Guru:
  Trigger: AOT/trimming guide needs update
  Signal: "New pattern discovered that needs documenting"
  Response: Update docs/contributing/aot-trimming-guide.md
  Hand-off: Pattern documented with examples

- WITH Development Agents:
  Trigger: Code changes affect public API
  Signal: "API changed - documentation needs update"
  Response: Update XML docs, API reference, examples
  Hand-off: Documentation synchronized with code

Part 3: Implementation Structure

Directory Layout

.claude/skills/technical-writer/
├── SKILL.md                      # Main skill documentation (1200-1500 lines)
├── README.md                     # Quick reference guide (400-500 lines)
├── MAINTENANCE.md                # Quarterly review process
├── scripts/
│   ├── link-validator.fsx        # Validate internal and external links
│   ├── example-freshness.fsx     # Check if code examples still compile/run
│   ├── doc-coverage.fsx          # Analyze documentation coverage
│   ├── style-checker.fsx         # Check documentation style consistency
│   ├── hugo-doctor.fsx           # Diagnose Hugo build issues
│   ├── diagram-validator.fsx     # Validate Mermaid/PlantUML diagrams
│   └── common.fsx                # Shared utilities
├── templates/
│   ├── content/
│   │   ├── api-doc-template.md       # API documentation template
│   │   ├── tutorial-template.md      # Tutorial/guide template
│   │   ├── adr-template.md           # Architecture Decision Record template
│   │   ├── whats-new-template.md     # What's New document template
│   │   ├── troubleshooting-template.md # Troubleshooting guide template
│   │   ├── concept-template.md       # Conceptual explanation template
│   │   └── example-template.md       # Code example template
│   ├── hugo/
│   │   ├── section-index.md          # Hugo section _index.md template
│   │   ├── frontmatter-guide.md      # Hugo frontmatter best practices
│   │   └── shortcode-examples.md     # Custom shortcode usage examples
│   └── diagrams/
│       ├── mermaid-flowchart.md      # Mermaid flowchart template
│       ├── mermaid-sequence.md       # Mermaid sequence diagram template
│       ├── mermaid-class.md          # Mermaid class diagram template
│       ├── mermaid-state.md          # Mermaid state diagram template
│       ├── mermaid-er.md             # Mermaid ER diagram template
│       ├── plantuml-architecture.md  # PlantUML architecture template
│       └── plantuml-component.md     # PlantUML component diagram template
└── patterns/
    ├── content/
    │   ├── api-documentation.md      # How to document APIs effectively
    │   ├── code-examples.md          # Code example best practices
    │   ├── cross-referencing.md      # Cross-reference patterns
    │   ├── versioning-docs.md        # Documentation versioning strategies
    │   ├── accessibility.md          # Accessible documentation patterns
    │   ├── error-messages.md         # How to document error messages
    │   ├── cli-documentation.md      # CLI command documentation patterns
    │   ├── configuration-docs.md     # Configuration documentation patterns
    │   └── migration-guides.md       # Migration guide patterns
    ├── hugo-docsy/
    │   ├── navigation-patterns.md    # Effective navigation structures
    │   ├── landing-pages.md          # Compelling landing page patterns
    │   ├── docsy-customization.md    # Docsy theme customization patterns
    │   ├── shortcodes-catalog.md     # Useful Hugo shortcodes
    │   ├── search-optimization.md    # Search and discoverability patterns
    │   └── troubleshooting-hugo.md   # Common Hugo issues and solutions
    └── visual/
        ├── diagram-selection.md      # When to use which diagram type
        ├── mermaid-best-practices.md # Mermaid diagram patterns
        ├── plantuml-best-practices.md # PlantUML diagram patterns
        ├── visual-storytelling.md    # Making concepts pop with visuals
        ├── consistent-styling.md     # Diagram and visual consistency
        └── architecture-diagrams.md  # Architecture visualization patterns

SKILL.md Structure (Target: 1200-1500 lines)

---
name: technical-writer
description: "Expert communication craftsperson for morphir-dotnet. Master of Hugo/Docsy, Mermaid/PlantUML diagrams, and technical writing. Use when user asks to create documentation, update docs, write tutorials, create diagrams, fix Hugo issues, customize Docsy, validate examples, check links, enforce style guide, or solve communication challenges. Triggers include 'document', 'docs', 'README', 'tutorial', 'example', 'API docs', 'style guide', 'link check', 'hugo', 'docsy', 'diagram', 'mermaid', 'plantuml', 'visual', 'navigation'."
# Common short forms: docs, writer, doc-writer (documentation only - aliases not functional)
---

# Technical Writer Skill

You are an expert communication craftsperson for the morphir-dotnet project. Your role
extends beyond documentation maintenance—you transform complex technical concepts into
clear, engaging, and visually compelling content that fosters understanding and helps
users succeed.

You are the go-to team member for:
- Solving communication challenges through writing
- Making Hugo and Docsy comply with project needs
- Creating diagrams and visuals that make ideas and concepts pop
- Applying patterns and templates from successful documentation sites
- Maintaining Morphir's consistent and well-crafted identity

[Content following the established pattern from other skills]

Automation Scripts (7 Scripts)

  • Purpose: Validate internal and external documentation links
  • Input: Documentation root directory (default: docs/)
  • Output: Report of broken links, redirects, and suggestions
  • Token Savings: ~600 tokens (vs manual link checking)

2. example-freshness.fsx

  • Purpose: Check if code examples still compile and produce expected output
  • Input: Examples directory or specific files
  • Output: Report of stale, broken, or outdated examples
  • Token Savings: ~800 tokens (vs manual example verification)

3. doc-coverage.fsx

  • Purpose: Analyze documentation coverage for public APIs
  • Input: Source code directories
  • Output: Coverage report with missing documentation
  • Token Savings: ~700 tokens (vs manual coverage analysis)

4. style-checker.fsx

  • Purpose: Check documentation against style guide
  • Input: Documentation files
  • Output: Style violations and suggestions
  • Token Savings: ~500 tokens (vs manual style review)

5. doc-sync-checker.fsx

  • Purpose: Detect documentation that’s out of sync with code
  • Input: Source and documentation directories
  • Output: Synchronization report
  • Token Savings: ~900 tokens (vs manual sync checking)

6. hugo-doctor.fsx

  • Purpose: Diagnose and troubleshoot Hugo build issues
  • Input: Hugo project directory (default: docs/)
  • Output: Diagnostic report with issue categorization and suggested fixes
  • Capabilities:
    • Detect frontmatter errors
    • Identify missing required fields
    • Check for broken shortcode references
    • Validate Hugo module configuration
    • Identify Docsy-specific issues
    • Check for common Hugo pitfalls
  • Token Savings: ~1000 tokens (vs manual Hugo troubleshooting)

7. diagram-validator.fsx

  • Purpose: Validate Mermaid and PlantUML diagrams in documentation
  • Input: Documentation files containing diagrams
  • Output: Report of invalid diagrams with syntax errors and suggestions
  • Capabilities:
    • Parse and validate Mermaid syntax
    • Check PlantUML diagram validity
    • Identify inconsistent styling
    • Suggest diagram improvements
    • Detect overly complex diagrams
  • Token Savings: ~700 tokens (vs manual diagram validation)

Part 4: Review Capability

Review Scope

The Technical Writer skill should proactively look for:

  1. Broken Links: Internal and external links that no longer work
  2. Stale Examples: Code examples that don’t compile or produce wrong output
  3. Missing Documentation: Public APIs without documentation
  4. Style Violations: Documentation not following style guide
  5. Outdated Content: Documentation that doesn’t match current behavior
  6. Orphaned Content: Documentation that’s no longer referenced
  7. Accessibility Issues: Content that isn’t accessible
  8. Translation Issues: Content with culture-specific idioms

Review Frequency

  • Continuous: Link validation on documentation changes (CI/CD)
  • Per-PR: Example freshness check for PRs touching examples
  • Weekly: Style consistency scan
  • Quarterly: Comprehensive documentation audit

Review Triggers

Trigger TypeWhenOutput
CI/CD PushDocumentation file changedLink validation report
PR ReviewPR includes documentationDoc quality checklist
Weekly ScheduleSunday midnightStyle compliance report
Quarterly ReviewFirst week of quarterComprehensive audit
Manual RequestUser invokes reviewFull documentation report
Release PreparationBefore releaseRelease docs checklist

Review Output Format

# Documentation Review Report

## Summary
- Total documents scanned: N
- Issues found: N
- Critical: N | High: N | Medium: N | Low: N

## Broken Links (Critical)
| File | Line | Link | Status |
|------|------|------|--------|
| docs/readme.md | 42 | [link](./missing.md) | 404 Not Found |

## Stale Examples (High)
| File | Example | Issue |
|------|---------|-------|
| docs/tutorials/getting-started.md | Code block L15-25 | Compilation error |

## Missing Documentation (Medium)
| Type | Name | Location |
|------|------|----------|
| Public API | Morphir.Core.Validate | src/Morphir.Core/Validate.fs |

## Style Violations (Low)
| File | Issue | Suggestion |
|------|-------|------------|
| docs/api/readme.md | Heading style | Use sentence case |

## Recommendations
1. Fix broken links immediately
2. Update stale examples in next sprint
3. Add XML docs to new public APIs
4. Schedule style cleanup

Integration with Retrospectives

Review → Findings → Retrospectives → Process Improvement

Example Flow:
1. Q1 Review finds 15 broken links
2. Retrospective: "Links break when files move"
3. Process Update: Add link check to PR checklist
4. Q2 Review finds 3 broken links (improvement!)
5. Pattern: Link validation at PR time prevents breakage

Part 5: Decision Trees

Decision Tree 1: “What type of diagram should I create?”

What are you trying to communicate?
├── Process or workflow
│   └── Use: Mermaid Flowchart
│       ├── Start/end nodes
│       ├── Decision diamonds
│       ├── Process rectangles
│       └── Directional arrows
│
├── Sequence of interactions (who calls whom)
│   └── Use: Mermaid Sequence Diagram
│       ├── Actors and participants
│       ├── Message arrows
│       ├── Activation boxes
│       └── Notes and loops
│
├── Object relationships and structure
│   └── Use: Mermaid Class Diagram
│       ├── Classes with attributes/methods
│       ├── Inheritance arrows
│       ├── Composition/aggregation
│       └── Interface implementations
│
├── State transitions
│   └── Use: Mermaid State Diagram
│       ├── States and transitions
│       ├── Entry/exit actions
│       ├── Nested states
│       └── Fork/join for parallel states
│
├── Data relationships
│   └── Use: Mermaid ER Diagram
│       ├── Entities and attributes
│       ├── Relationships with cardinality
│       └── Primary/foreign keys
│
├── System architecture (high-level)
│   └── Use: Mermaid Flowchart with subgraphs
│       ├── Components as subgraphs
│       ├── Data flow arrows
│       └── Clear boundaries
│
├── System architecture (detailed)
│   └── Use: PlantUML Component/Deployment Diagram
│       ├── Components with interfaces
│       ├── Dependencies
│       ├── Deployment nodes
│       └── Technology annotations
│
├── Timeline or project plan
│   └── Use: Mermaid Gantt Chart
│       ├── Tasks and durations
│       ├── Dependencies
│       ├── Milestones
│       └── Sections
│
└── User journey or experience
    └── Use: Mermaid Journey Diagram
        ├── Journey stages
        ├── Actions per stage
        ├── Satisfaction scores
        └── Actor perspective

Decision Tree 2: “Hugo is not building - what do I check?”

Hugo build failing?
├── Error mentions "module"
│   └── Hugo module issue
│       ├── Run: hugo mod tidy
│       ├── Run: hugo mod get -u
│       ├── Check: go.mod and go.sum exist
│       └── Verify: Network access to GitHub
│
├── Error mentions "template" or "shortcode"
│   └── Template/shortcode issue
│       ├── Check: Shortcode exists in layouts/shortcodes/
│       ├── Check: Docsy shortcode name (alert vs warning)
│       ├── Verify: Closing tags match opening tags
│       └── Look for: Unclosed shortcode delimiters
│
├── Error mentions "frontmatter" or "YAML"
│   └── Frontmatter issue
│       ├── Check: Valid YAML syntax
│       ├── Verify: Required fields (title, linkTitle)
│       ├── Look for: Tabs vs spaces issues
│       └── Check: Special characters need quoting
│
├── Error mentions "taxonomy" or "term"
│   └── Taxonomy issue
│       ├── Check: hugo.toml taxonomies config
│       ├── Verify: Taxonomy pages exist
│       └── Check: Singular vs plural naming
│
├── Error mentions "page not found" or "ref"
│   └── Reference issue
│       ├── Check: Target page exists
│       ├── Verify: Path is relative to content/
│       ├── Use: relref instead of ref for sections
│       └── Check: Case sensitivity
│
├── Site builds but looks wrong
│   └── Docsy/styling issue
│       ├── Check: Docsy module version
│       ├── Verify: assets/scss/custom.scss syntax
│       ├── Check: layouts/ override conflicts
│       └── Clear: hugo cache (resources/_gen/)
│
└── Site builds but navigation is wrong
    └── Navigation issue
        ├── Check: _index.md files in sections
        ├── Verify: weight in frontmatter
        ├── Check: linkTitle for menu display
        └── Review: hugo.toml menu configuration

Decision Tree 3: “What type of documentation should I create?”

What are you documenting?
├── Public API (class, method, interface)
│   └── Create: XML doc comments + API reference page
│       ├── Parameters and return values
│       ├── Exceptions thrown
│       ├── Code example
│       └── See also references
│
├── Feature or capability
│   └── Create: Conceptual guide + tutorial
│       ├── What it does (conceptual)
│       ├── How to use it (tutorial)
│       ├── Examples (code samples)
│       └── Troubleshooting (common issues)
│
├── Configuration or setup
│   └── Create: Configuration reference + getting started
│       ├── All options documented
│       ├── Default values
│       ├── Examples for common scenarios
│       └── Validation and error messages
│
├── CLI command
│   └── Create: Command reference + usage examples
│       ├── Synopsis with all options
│       ├── Detailed option descriptions
│       ├── Examples for each use case
│       └── Exit codes and errors
│
├── Architecture decision
│   └── Create: ADR (Architecture Decision Record)
│       ├── Context and problem
│       ├── Decision and rationale
│       ├── Consequences
│       └── Status and date
│
└── Breaking change
    └── Create: Migration guide
        ├── What changed
        ├── Why it changed
        ├── How to migrate
        └── Deprecation timeline

Decision Tree 2: “Is this documentation good enough?”

Documentation Quality Checklist:

1. Accuracy
   └── Does it match current behavior?
       YES → Continue
       NO → Update or flag for update

2. Completeness
   └── Does it cover all aspects?
       ├── Happy path? ✓
       ├── Edge cases? ✓
       ├── Errors? ✓
       └── Examples? ✓

3. Clarity
   └── Can target audience understand it?
       ├── No jargon without explanation ✓
       ├── Logical structure ✓
       ├── Visual aids where helpful ✓
       └── Scannable headings ✓

4. Discoverability
   └── Can users find it?
       ├── In navigation ✓
       ├── Proper keywords/tags ✓
       ├── Cross-referenced ✓
       └── Linked from related docs ✓

5. Maintainability
   └── Will it stay accurate?
       ├── Code examples tested ✓
       ├── Links validated ✓
       ├── No hard-coded versions ✓
       └── Owner assigned ✓

Decision Tree 3: “How should I handle outdated documentation?”

Is the documentation outdated?
├── Minor inaccuracy (typo, small detail)
│   └── Fix immediately in same PR
│
├── Moderate drift (feature changed slightly)
│   └── Create issue to track update
│       ├── Label: documentation
│       ├── Priority: medium
│       └── Link to related code change
│
├── Major drift (feature significantly changed)
│   └── Coordinate with feature owner
│       ├── Understand new behavior
│       ├── Rewrite documentation
│       ├── Update all examples
│       └── Create migration guide if breaking
│
├── Feature removed
│   └── Deprecation workflow
│       ├── Mark as deprecated (if applicable)
│       ├── Add removal notice
│       ├── Schedule removal date
│       └── Remove after grace period
│
└── Unsure if outdated
    └── Verify against code
        ├── Run examples
        ├── Check API signatures
        ├── Test documented behavior
        └── Flag for review if uncertain

Part 6: Playbooks

Playbook 1: New Feature Documentation

When: A new feature is being implemented or has been implemented

Prerequisites:

  • Feature PR is available or merged
  • Feature behavior is understood
  • Target audience identified

Steps:

  1. Understand the feature

    • Read PR description and linked issues
    • Review code changes
    • Identify public APIs
    • Note configuration options
  2. Plan documentation

    • Identify documentation types needed:
      • API reference (XML docs)
      • Conceptual guide
      • Tutorial
      • Configuration reference
      • CLI command reference (if applicable)
    • Determine target audience
    • Plan examples needed
  3. Create API documentation

    • Add XML doc comments to all public members
    • Include <summary>, <param>, <returns>, <exception>
    • Add <example> blocks for non-obvious usage
    • Add <seealso> references
  4. Create user-facing documentation

    • Write conceptual overview (what and why)
    • Create step-by-step tutorial (how)
    • Add code examples (tested and working)
    • Document configuration options
    • Add troubleshooting section
  5. Integrate with existing documentation

    • Add to navigation/table of contents
    • Cross-reference from related documents
    • Update What’s New (if for upcoming release)
    • Update README if feature is major
  6. Validate documentation

    • Run link validator
    • Test all code examples
    • Review for style compliance
    • Get peer review

Output: Complete documentation package for the feature


Playbook 2: Documentation Audit

When: Quarterly review or before major release

Prerequisites:

  • Access to documentation and source code
  • Style guide available
  • Previous audit results (if available)

Steps:

  1. Run automated checks

    dotnet fsi .claude/skills/technical-writer/scripts/link-validator.fsx
    dotnet fsi .claude/skills/technical-writer/scripts/example-freshness.fsx
    dotnet fsi .claude/skills/technical-writer/scripts/doc-coverage.fsx
    dotnet fsi .claude/skills/technical-writer/scripts/style-checker.fsx
    
  2. Review automated findings

    • Categorize by severity (Critical, High, Medium, Low)
    • Identify patterns in issues
    • Prioritize fixes
  3. Manual review checklist

    • Navigation makes sense
    • Getting started guide works end-to-end
    • All CLI commands documented
    • Configuration options complete
    • Error messages explained
    • Troubleshooting covers common issues
    • Examples are realistic and useful
  4. Create audit report

    • Summary of findings
    • Comparison to previous audit (if available)
    • Prioritized fix list
    • Recommendations for process improvements
  5. Create fix plan

    • Create issues for each category of fix
    • Assign owners
    • Set target dates
    • Schedule follow-up review

Output: Documentation audit report with action items


Playbook 3: Release Documentation

When: Release is being prepared

Prerequisites:

  • CHANGELOG.md has been updated
  • Release version determined
  • Feature list finalized

Steps:

  1. Review changelog

    • Ensure all user-facing changes documented
    • Verify categorization (Added, Changed, Fixed, Breaking)
    • Check formatting follows Keep a Changelog
  2. Create/Update What’s New

    • Copy from CHANGELOG with more detail
    • Add code examples for new features
    • Include migration guides for breaking changes
    • Add screenshots/diagrams where helpful
  3. Update version references

    • Search for hard-coded versions
    • Update installation instructions
    • Update compatibility matrices
    • Update getting started guides
  4. Validate all documentation

    • Run full link check
    • Test all examples with new version
    • Verify navigation works
    • Check mobile/responsive rendering
  5. Coordinate with Release Manager

    • Confirm documentation is ready
    • Provide documentation checklist
    • Hand off for release publication

Output: Release-ready documentation


Playbook 4: Example Code Maintenance

When: Examples are reported as broken or during regular maintenance

Prerequisites:

  • Access to example code
  • Build environment available
  • Understanding of what examples should demonstrate

Steps:

  1. Inventory examples

    • List all code examples in documentation
    • Categorize by type (inline, file-based, repository)
    • Note dependencies
  2. Test examples

    dotnet fsi .claude/skills/technical-writer/scripts/example-freshness.fsx
    
  3. Fix broken examples

    • Update syntax for API changes
    • Fix compilation errors
    • Update expected output
    • Ensure examples still demonstrate intended concept
  4. Improve examples

    • Add error handling where appropriate
    • Use realistic data
    • Add comments explaining key points
    • Keep examples focused and minimal
  5. Add example testing to CI

    • Extract testable examples
    • Add to test suite
    • Configure CI to run example tests

Output: All examples working and tested


Playbook 5: Hugo/Docsy Troubleshooting

When: Hugo build fails or documentation site doesn’t render correctly

Prerequisites:

  • Hugo installed (hugo version)
  • Go installed (for Hugo modules)
  • Access to docs/ directory

Steps:

  1. Capture the error

    cd docs
    hugo --verbose 2>&1 | tee hugo-build.log
    
  2. Run hugo-doctor script

    dotnet fsi .claude/skills/technical-writer/scripts/hugo-doctor.fsx
    
  3. Check Hugo modules

    hugo mod graph        # Show module dependencies
    hugo mod tidy         # Clean up go.mod
    hugo mod get -u       # Update all modules
    
  4. Check Docsy-specific issues

    • Verify go.mod references Docsy correctly
    • Check for layouts/ overrides conflicting with Docsy
    • Review assets/scss/ for SCSS syntax errors
    • Verify static/ files aren’t conflicting
  5. Clear caches and rebuild

    rm -rf resources/_gen/
    rm -rf public/
    hugo --gc --minify
    
  6. Common fixes

    • Frontmatter: Use title: not Title:
    • Navigation: Ensure _index.md in each section
    • Links: Use Hugo ref shortcode or relative paths
    • Shortcodes: Use percent-delimiters for markdown processing, angle-bracket delimiters for HTML output
  7. Document the fix

    • Add to patterns/hugo-docsy/troubleshooting-hugo.md
    • Update team if common issue

Output: Working Hugo build with documented fix


Playbook 6: Creating Effective Diagrams

When: Need to visualize a concept, architecture, or process

Prerequisites:

  • Clear understanding of what needs to be communicated
  • Knowledge of target audience

Steps:

  1. Identify the purpose

    • What question does this diagram answer?
    • Who is the audience?
    • What level of detail is needed?
  2. Choose diagram type (see Decision Tree 1)

    • Process → Flowchart
    • Interactions → Sequence diagram
    • Structure → Class diagram
    • States → State diagram
    • Data → ER diagram
    • Architecture → Component diagram
  3. Sketch rough layout

    • Start with pen/paper or whiteboard
    • Identify key elements
    • Determine relationships
    • Plan visual flow (top-to-bottom or left-to-right)
  4. Create in Mermaid/PlantUML

    ```mermaid
    graph TD
        A[Start] --> B{Decision}
        B -->|Yes| C[Action 1]
        B -->|No| D[Action 2]
        C --> E[End]
        D --> E
    
  5. Apply consistent styling

    • Use project color scheme
    • Consistent node shapes
    • Clear, readable labels
    • Appropriate arrow styles
  6. Review and simplify

    • Remove unnecessary details
    • Group related elements
    • Add legend if needed
    • Test rendering in Hugo
  7. Add context

    • Caption explaining the diagram
    • Reference in surrounding text
    • Link to related documentation

Output: Clear, compelling diagram that communicates effectively


Playbook 7: Docsy Theme Customization

When: Need to customize documentation site appearance or behavior

Prerequisites:

  • Understanding of Docsy theme structure
  • Access to docs/ directory

Steps:

  1. Understand Docsy structure

    docs/
    ├── hugo.toml              # Main configuration
    ├── go.mod                 # Hugo modules (including Docsy)
    ├── assets/
    │   └── scss/
    │       └── _variables_project.scss  # Color/style overrides
    ├── layouts/               # Template overrides
    │   └── partials/          # Partial template overrides
    ├── static/                # Static assets
    └── content/               # Documentation content
    
  2. For color/styling changes

    • Edit assets/scss/_variables_project.scss
    • Override Docsy SCSS variables
    • Don’t modify Docsy files directly (they’re in go modules)
  3. For layout changes

    • Copy Docsy template to layouts/ with same path
    • Modify the copy (original stays in module)
    • Test thoroughly - Docsy updates may conflict
  4. For navigation changes

    • Configure in hugo.toml under [menu]
    • Use weight in frontmatter for ordering
    • Use _index.md files for section pages
  5. For new shortcodes

    • Create in layouts/shortcodes/
    • Name file shortcodename.html
    • Reference in content using angle-bracket shortcode syntax
  6. Test changes

    hugo server -D  # Include drafts
    # Check at http://localhost:1313
    
  7. Document customizations

    • Add to patterns/hugo-docsy/docsy-customization.md
    • Explain why customization was needed
    • Note any Docsy version dependencies

Output: Customized documentation site with documented changes


Part 7: Pattern Catalog (Seed Patterns)

Pattern 1: API Documentation Structure

Context: Documenting a public API (class, method, interface)

Pattern:

/// <summary>
/// Brief one-line description of what this does.
/// </summary>
/// <remarks>
/// Extended explanation if needed.
/// Use when you need to explain concepts, caveats, or usage patterns.
/// </remarks>
/// <param name="paramName">Description of parameter including valid values.</param>
/// <returns>Description of return value, including null/empty cases.</returns>
/// <exception cref="ArgumentException">When paramName is invalid because...</exception>
/// <example>
/// <code>
/// var result = MyMethod("value");
/// // Use result for...
/// </code>
/// </example>
/// <seealso cref="RelatedClass"/>
/// <seealso href="https://docs.example.com/concept">Concept explanation</seealso>

Anti-pattern:

/// <summary>
/// Gets the thing.
/// </summary>
// Missing: what thing, when to use, what could go wrong

Pattern 2: Tutorial Structure

Context: Writing a step-by-step tutorial

Pattern:

# Tutorial: [Action] with [Feature]

## Overview
What you'll learn and what you'll build.

## Prerequisites
- Requirement 1
- Requirement 2

## Step 1: [First action]
Explanation of what and why.

```code
Example code

Expected result: [what user should see]

Step 2: [Next action]

…

Summary

What was accomplished.

Next Steps

  • Related tutorial 1
  • Related concept guide
  • API reference

Troubleshooting

Issue: [Common problem]

Solution: How to fix it.


---

### Pattern 3: CLI Command Documentation

**Context**: Documenting a CLI command

**Pattern**:
```markdown
# command-name

Brief description of what command does.

## Synopsis

command-name [options] [optional-arg]


## Description

Extended description explaining:
- Purpose and use cases
- How it relates to other commands
- Important concepts

## Arguments

| Argument | Description | Required |
|----------|-------------|----------|
| `<required-arg>` | Description | Yes |
| `[optional-arg]` | Description | No |

## Options

| Option | Shorthand | Description | Default |
|--------|-----------|-------------|---------|
| `--verbose` | `-v` | Enable verbose output | false |

## Examples

### Basic usage
```bash
command-name input.json

With options

command-name --verbose --format json input.json

Exit Codes

CodeMeaning
0Success
1Validation error
2Runtime error

See Also


---

### Pattern 4: Error Message Documentation

**Context**: Documenting error messages and troubleshooting

**Pattern**:
```markdown
## Error: ERROR_CODE - Brief Description

### Message

The exact error message users see


### Cause
Explanation of what causes this error.

### Solution
1. First thing to try
2. Second thing to try
3. If still failing, check...

### Example
```bash
# Command that causes error
$ morphir verify invalid.json
Error: INVALID_SCHEMA - Schema validation failed

# How to fix
$ morphir verify --schema v3 valid.json

---

### Pattern 5: Configuration Documentation

**Context**: Documenting configuration options

**Pattern**:
```markdown
# Configuration Reference

## Overview
Brief explanation of configuration system.

## Configuration File
Location: `morphir.config.json` or `package.json` under `morphir` key

## Options

### `optionName`
- **Type**: `string | string[]`
- **Default**: `"default value"`
- **Required**: No
- **Since**: v1.2.0

Description of what this option does and when to use it.

**Valid Values**:
- `"value1"` - Description
- `"value2"` - Description

**Example**:
```json
{
  "optionName": "value1"
}

Notes:

  • Special consideration 1
  • Special consideration 2

---

### Pattern 6: Cross-Reference Best Practices

**Context**: Linking between documentation pages

**Pattern**:
- Use relative paths: `[Link text](./related.md)` not absolute URLs
- Link to specific sections: `[Section](./page.md#section-id)`
- Use descriptive link text: `[how to configure X](./config.md)` not `[click here](./config.md)`
- Add "See also" sections at end of documents
- Cross-link from conceptual to API to tutorial

**Anti-pattern**:
- `[here](./page.md)` - non-descriptive
- `https://github.com/.../docs/page.md` - will break on forks
- No cross-references - orphaned content

---

### Pattern 7: Mermaid Flowchart Best Practices

**Context**: Creating process or workflow diagrams

**Pattern**:
```mermaid
graph TD
    subgraph "Input Phase"
        A[Start] --> B{Validate Input}
    end

    subgraph "Processing Phase"
        B -->|Valid| C[Process Data]
        B -->|Invalid| D[Handle Error]
        C --> E{Check Result}
    end

    subgraph "Output Phase"
        E -->|Success| F[Return Result]
        E -->|Failure| D
        D --> G[Log Error]
        G --> H[End]
        F --> H
    end

    style A fill:#90EE90
    style H fill:#FFB6C1
    style D fill:#FFD700

Best Practices:

  • Use subgraphs to group related steps
  • Consistent node shapes: rectangles for actions, diamonds for decisions
  • Color code: green for start, red for end, yellow for errors
  • Label edges with conditions
  • Flow top-to-bottom or left-to-right
  • Keep diagrams focused - split if > 15 nodes

Anti-pattern:

  • No grouping - flat, hard-to-follow diagram
  • Inconsistent shapes - confuses readers
  • Missing edge labels on decisions
  • Overly complex - trying to show everything

Pattern 8: Mermaid Sequence Diagram Best Practices

Context: Showing interactions between components/actors

Pattern:

sequenceDiagram
    autonumber
    participant U as User
    participant CLI as Morphir CLI
    participant V as Validator
    participant FS as File System

    U->>CLI: morphir verify input.json
    activate CLI
    CLI->>FS: Read input file
    FS-->>CLI: File contents
    CLI->>V: Validate(content)
    activate V
    V->>V: Parse JSON
    V->>V: Check schema
    alt Valid
        V-->>CLI: ValidationResult.Success
    else Invalid
        V-->>CLI: ValidationResult.Errors
    end
    deactivate V
    CLI-->>U: Display result
    deactivate CLI

Best Practices:

  • Use autonumber for step references
  • Name participants clearly with aliases
  • Show activation bars for processing time
  • Use alt/else for conditional flows
  • Use loop for repeated operations
  • Add notes for important clarifications
  • Keep interactions readable (< 20 messages)

Pattern 9: Hugo Frontmatter Best Practices

Context: Setting up Hugo page frontmatter

Pattern:

---
title: "Page Title for SEO and Browser Tab"
linkTitle: "Short Nav Title"
description: "One-line description for search results and social sharing"
weight: 10
date: 2025-01-15
lastmod: 2025-01-20
draft: false
toc: true
categories:
  - Guides
tags:
  - getting-started
  - tutorial
---

Field Guidelines:

FieldPurposeBest Practice
titleSEO, browser tabDescriptive, include keywords
linkTitleNavigation menuShort (2-4 words)
descriptionSearch/social previewSingle sentence, < 160 chars
weightMenu orderingLower = higher in menu
dateCreation dateISO 8601 format
lastmodLast modificationAuto if enableGitInfo=true
draftHide from buildSet false when ready
tocTable of contentstrue for long pages

Anti-pattern:

  • Missing linkTitle - navigation shows full title
  • No description - poor search results
  • Random weights - chaotic navigation
  • Draft pages in production

Pattern 10: Visual Storytelling

Context: Explaining complex concepts with visuals

Pattern: The “Zoom In” Technique

  1. Start with the big picture

    • High-level architecture diagram
    • 3-5 main components
    • No implementation details
  2. Then zoom into details

    • Detailed view of each component
    • Show interfaces and interactions
    • Include relevant code snippets
  3. Connect back to the whole

    • Reference the big picture
    • Explain how detail fits in
    • Link to related detailed views

Example Structure:

## Architecture Overview

Here's how the Morphir pipeline works at a high level:

[High-level flowchart - 5 boxes]

Let's dive into each stage...

### Stage 1: Input Processing

This stage handles [description]. Here's a closer look:

[Detailed sequence diagram for Stage 1]

This connects to Stage 2 via [interface description].

### Stage 2: Validation

[Continue pattern...]

Why This Works:

  • Readers understand context first
  • Details make sense within the whole
  • Easy to navigate to specific areas
  • Supports different reading depths

Pattern 11: Docsy Navigation Structure

Context: Organizing documentation for discoverability

Pattern:

docs/content/
├── _index.md                    # Landing page
├── docs/
│   ├── _index.md               # Docs section landing
│   ├── getting-started/
│   │   ├── _index.md           # Getting started landing
│   │   ├── installation.md     # weight: 1
│   │   └── quickstart.md       # weight: 2
│   ├── guides/
│   │   ├── _index.md           # Guides landing
│   │   └── [topic guides...]
│   ├── reference/
│   │   ├── _index.md           # Reference landing
│   │   ├── cli/
│   │   └── api/
│   └── concepts/
│       ├── _index.md           # Concepts landing
│       └── [concept pages...]
├── contributing/
│   └── _index.md               # Contributing section
└── about/
    └── _index.md               # About section

Key Principles:

  • Every directory needs _index.md
  • Use weight for consistent ordering
  • Group by user intent (getting started, guides, reference)
  • Separate conceptual from procedural content
  • Keep nesting to 3 levels max

Part 8: Token Savings Analysis

Automation Script Token Savings

ScriptManual TokensAutomated TokensSavings
link-validator.fsx~800~200~600 (75%)
example-freshness.fsx~1000~200~800 (80%)
doc-coverage.fsx~900~200~700 (78%)
style-checker.fsx~700~200~500 (71%)
doc-sync-checker.fsx~1100~200~900 (82%)
hugo-doctor.fsx~1200~200~1000 (83%)
diagram-validator.fsx~900~200~700 (78%)

Total per audit cycle: ~5200 tokens saved

Quarterly Review Savings

ActivityManualAutomatedSavings
Link validation2 hours5 min115 min
Example testing4 hours15 min225 min
Coverage analysis2 hours10 min110 min
Style checking3 hours10 min170 min
Hugo troubleshooting2 hours5 min115 min
Diagram validation1 hour5 min55 min

Total quarterly: ~13 hours saved per quarter


Part 9: Success Criteria

Phase 1: Alpha (Initial Delivery)

  • Directory structure created (including hugo/, diagrams/ subdirectories)
  • SKILL.md complete (1200+ lines)
  • README.md created (400-500 lines)
  • MAINTENANCE.md documented
  • 7 automation scripts implemented
  • 11+ seed patterns documented (including Hugo/Docsy and visual patterns)
  • 14+ templates created (content, hugo, diagrams)
  • Coordination points mapped
  • Cross-agent compatibility verified
  • Hugo/Docsy expertise demonstrated
  • Mermaid/PlantUML diagram examples included

Phase 2: Beta (After First Quarter)

  • Feedback capture mechanism working
  • Quarterly review completed
  • 18+ patterns in catalog
  • Scripts tested on real documentation
  • Coordination with other gurus tested
  • At least 2 audits completed successfully
  • Hugo troubleshooting playbook validated
  • Diagram creation playbook used successfully

Phase 3: Stable (After Two Quarters)

  • 25+ patterns in catalog
  • Proven reliable over 2+ quarters
  • Automated feedback generating insights
  • Token efficiency documented
  • Cross-project reuse strategy documented
  • Integration with other gurus proven
  • Hugo/Docsy best practices established and documented
  • Visual identity guidelines in place
  • Documentation site improvements measurable

Part 10: Integration Requirements

Registration Locations

When implemented, the Technical Writer skill must be registered in:

  1. CLAUDE.md - Add to skill invocation section
  2. .agents/skills-reference.md - Full skill documentation
  3. .agents/skill-matrix.md - Maturity tracking entry
  4. .agents/capabilities-matrix.md - Cross-agent compatibility
  5. .github/copilot-instructions.md - Copilot-specific guidance
  6. .cursorrules - Cursor-specific triggers
  7. .windsurf/rules.md - Windsurf configuration (if exists)

CI/CD Integration

Consider adding to CI/CD:

  • Link validation on documentation PRs
  • Example compilation check
  • Style guide enforcement (advisory)

Documentation Structure Updates

May need updates to:

  • docs/ directory structure
  • Navigation/table of contents
  • README.md (project root)
  • CONTRIBUTING.md (contribution guidelines)

  • Issue #277 - Technical Writer skill implementation

Appendix B: References

Internal References

External References

Inspiration and Best Practices


Document History:

  • 2025-12-19: Initial draft created (v0.1.0)
  • 2025-12-19: Expanded with Hugo/Docsy expertise, visual communication (Mermaid/PlantUML), brand identity, and communication craftsperson role (v0.2.0)

16.1.6 - Product Requirements Documents

Feature specifications and implementation tracking for Morphir .NET

Product Requirements Documents (PRDs) track feature requirements, design decisions, and implementation status for all major features in Morphir .NET.

Active PRDs

PRDStatusDescription
IR JSON Schema Verification🚧 In ProgressSchema validation for Morphir IR
IR JSON Schema Verification BDD🚧 In ProgressBDD scenarios for schema verification
Deployment Architecture Refactor📋 DraftBuild and deployment improvements
F# backend / Myriad deliveryIn ProgressClassic v1-v3 to F#, per-module Myriad generation and car rental tutorial
Layered ConfigurationIn ProgressCampaigns 1 through 5 landed and cleaned; workspace migration locally qualified and archived under #479; parent review/CI/landing/cleanup pending
Product Manager Skill📋 DraftAI skill for product management

Status Legend

StatusMeaning
📋 DraftInitial PRD being refined, not yet approved
✅ ApprovedPRD reviewed and ready for implementation
🚧 In ProgressActive implementation underway
✓ CompletedAll features implemented, PRD archived
⏸️ DeferredPRD postponed with reason and timeline

How to Use PRDs

For Contributors

  1. Starting Work: Check the status to see what’s being worked on
  2. Implementation: Update the PRD’s Feature Status Tracking table as you complete features
  3. Design Decisions: Add Implementation Notes to capture important decisions
  4. Questions: Document answers to Open Questions as they’re resolved

For AI Agents

When asked “What should I work on?” or “What’s the current status?”:

  1. Check this index for active PRDs
  2. Open the relevant PRD and find the Feature Status Tracking table
  3. Look for features with status ⏳ Planned (ready to start) or 🚧 In Progress
  4. Update feature status in real-time as work progresses
  5. Add Implementation Notes for significant design decisions

Creating a New PRD

  1. Copy an existing PRD as a template
  2. Fill in all sections with comprehensive detail
  3. Include Feature Status Tracking table with all planned features
  4. Add to this index with “Draft” status
  5. Submit for review and approval before implementation begins

16.1.6.1 - PRD: IR JSON Schema Verification

Product Requirements Document for Morphir IR JSON schema verification tooling

Product Requirements Document: IR JSON Schema Verification

Status: ✅ Phase 1 Complete | ⏳ Phase 2 Ready Created: 2025-12-13 Last Updated: 2025-12-15 Phase 1 Completion Date: 2025-12-15 Current Phase: Phase 1 Complete - Ready for Phase 2 Author: Morphir .NET Team

Overview

This PRD defines the requirements for adding JSON schema verification capabilities to the Morphir .NET CLI and tooling. This feature will enable developers to validate Morphir IR JSON files against the official schema specifications for all supported format versions (v1, v2, v3).

The implementation will introduce WolverineFx as a messaging layer between the CLI and core tooling services, using Vertical Slice Architecture to organize features by use case rather than technical layers.

Problem Statement

Currently, developers working with Morphir IR JSON files have no built-in way to:

  1. Validate IR correctness: Verify that generated or hand-written IR files conform to the expected schema
  2. Debug format issues: Quickly identify structural problems in IR files
  3. Ensure version compatibility: Confirm which schema version an IR file uses and whether it’s valid
  4. Catch errors early: Detect malformed IR before it causes runtime failures in downstream tools

Current Pain Points

  • Manual validation: Developers must use external tools (Python jsonschema, Node.js ajv-cli) to validate IR
  • Version confusion: No automated way to detect which schema version an IR file uses
  • Poor error messages: External validators provide generic JSON schema errors without Morphir-specific context
  • Workflow friction: Validation requires switching between tools and languages

Goals

Primary Goals

  1. Enable IR validation via CLI command for all supported schema versions (v1, v2, v3)
  2. Establish WolverineFx integration with Vertical Slice Architecture as the foundation for future tooling commands
  3. Provide excellent developer experience with clear, actionable error messages and multiple output formats
  4. Support flexible input starting with file paths, with extensibility for stdin and multiple files
  5. Auto-detect schema versions while allowing manual override when needed

Secondary Goals

  1. Create reusable validation services in Morphir.Tooling that can be leveraged by other tools
  2. Establish testing patterns using BDD scenarios for validation use cases
  3. Document architectural decisions for Vertical Slice Architecture adoption

Non-Goals

Explicitly Out of Scope

  • IR migration/upgrade tooling: Will be addressed in a separate PRD (tracked below)
  • Schema generation: Creating schemas from .NET types
  • Real-time validation: IDE plugins or language servers
  • IR parsing/deserialization: This already exists in Morphir.Core
  • Schema authoring: Schemas are maintained in the upstream Morphir repository

User Stories

Story 1: Validate IR File

As a Morphir developer I want to validate my IR JSON file against the official schema So that I can catch structural errors before using the IR in other tools

Acceptance Criteria:

  • User runs morphir ir verify path/to/morphir-ir.json
  • Tool auto-detects schema version from JSON
  • Tool validates against appropriate schema
  • Tool returns clear success or detailed error messages
  • Exit code is 0 for valid, non-zero for invalid

Story 2: Validate Specific Schema Version

As a Morphir tooling developer I want to validate IR against a specific schema version So that I can test version-specific compatibility

Acceptance Criteria:

  • User runs morphir ir verify --schema-version 3 path/to/morphir-ir.json
  • Tool validates against specified schema version regardless of file content
  • Tool reports validation results for the specified version

Story 3: Machine-Readable Output

As a CI/CD pipeline I want to get validation results in JSON format So that I can parse and process errors programmatically

Acceptance Criteria:

  • User runs morphir ir verify --json path/to/morphir-ir.json
  • Tool outputs structured JSON with validation results
  • JSON includes error locations, messages, and metadata

Story 4: Quick Status Check

As a developer in a CI pipeline I want to validate IR without verbose output So that I can keep build logs clean

Acceptance Criteria:

  • User runs morphir ir verify --quiet path/to/morphir-ir.json
  • Tool only outputs errors (if any)
  • Exit code indicates success/failure

Story 5: Detect IR Version

As a Morphir developer I want to identify which schema version my IR file uses So that I know which tools and features are compatible

Acceptance Criteria:

  • User runs morphir ir detect-version path/to/morphir-ir.json
  • Tool analyzes IR structure and reports detected version
  • Tool provides confidence level or rationale for detection

Detailed Requirements

Functional Requirements

FR-1: Command Interface

Command Structure:

morphir ir verify <file-path> [options]

Required Arguments:

  • <file-path>: Path to the Morphir IR JSON file to validate

Options:

  • --schema-version <version>: Explicitly specify schema version (1, 2, or 3)
  • --json: Output results in JSON format
  • --quiet: Suppress output except errors
  • -v, --verbose: Show detailed validation information

Exit Codes:

  • 0: Validation successful
  • 1: Validation failed (schema errors)
  • 2: Operational error (file not found, invalid JSON, etc.)

FR-2: Input Format Support

Phase 1 (Initial Release):

  • ✅ File paths (absolute and relative)

Phase 2 (Future):

  • ⏳ Stdin support: cat morphir-ir.json | morphir ir verify -
  • ⏳ Multiple files: morphir ir verify file1.json file2.json file3.json
  • ⏳ Directory validation: morphir ir verify --recursive ./ir-files/

FR-3: Schema Version Handling

Auto-Detection Logic (default behavior):

  1. Look for formatVersion field in JSON
  2. Analyze tag capitalization patterns:
    • All lowercase tags → v1
    • Mixed capitalization → v2
    • All capitalized tags → v3
  3. If ambiguous, report detection failure with suggestions

Manual Override:

  • --schema-version option forces validation against specified version
  • Useful for testing migration scenarios
  • Bypasses auto-detection

FR-4: Output Formats

Human-Readable Format (default):

✓ Validation successful
  File: morphir-ir.json
  Schema: v3 (auto-detected)
  Validated: 2025-12-13 10:30:45

Or on error:

✗ Validation failed against schema v3

Error 1: Invalid type tag
  Path: $.modules[0].types["MyType"].value[0]
  Expected: "Public" or "Private"
  Found: "public"
  Line: 42, Column: 12

Error 2: Missing required field
  Path: $.package.modules
  Message: Required property 'name' is missing

2 errors found. Fix these issues and try again.

JSON Format (--json flag):

{
  "valid": false,
  "schemaVersion": "3",
  "detectionMethod": "auto",
  "file": "morphir-ir.json",
  "timestamp": "2025-12-13T10:30:45Z",
  "errors": [
    {
      "path": "$.modules[0].types[\"MyType\"].value[0]",
      "message": "Invalid type tag",
      "expected": ["Public", "Private"],
      "found": "public",
      "line": 42,
      "column": 12
    }
  ],
  "errorCount": 2
}

Quiet Format (--quiet flag):

  • Only output errors
  • No success messages
  • Minimal formatting

FR-5: Schema Validation

Supported Versions:

  • ✅ v1: morphir-ir-v1.yaml (all lowercase tags)
  • ✅ v2: morphir-ir-v2.yaml (mixed capitalization)
  • ✅ v3: morphir-ir-v3.yaml (all capitalized tags)

Validation Requirements:

  • Use json-everything library
  • Validate against JSON Schema Draft 07 specification
  • Provide detailed error locations using JSON Path notation
  • Include contextual information in error messages

FR-6: Version Detection Helper

Command:

morphir ir detect-version <file-path>

Output Example:

Detected schema version: v3
Confidence: High
Rationale:
  - All tags are capitalized ("Library", "Public", "Apply", etc.)
  - Contains formatVersion: 3

Implementation Status: ⏳ Planned for Phase 2

FR-7: Error Reporting Quality

Error Messages Must Include:

  • JSON Path to the error location
  • Expected value/format
  • Actual value found
  • Line and column numbers (when possible)
  • Suggested fixes (when applicable)

Example Error:

Error: Invalid access control tag
  Location: $.modules[0].types["Account"].accessControlled[0]
  Expected: One of ["Public", "Private"]
  Found: "public"
  Suggestion: Change "public" to "Public" (capitalize first letter)

Non-Functional Requirements

NFR-1: Performance

Targets:

  • Small files (<100KB): Validation completes in <100ms
  • Typical files (<1MB): Validation completes in <500ms
  • Large files (>1MB): Validation completes in <2 seconds

Benchmarking:

  • Use BenchmarkDotNet for performance testing
  • Test with representative IR files of varying sizes
  • Profile schema loading and validation separately

NFR-2: Reliability

Error Handling:

  • Gracefully handle malformed JSON with clear error messages
  • Catch and report file I/O errors (file not found, permission denied, etc.)
  • Handle edge cases: empty files, extremely large files, invalid UTF-8
  • Never crash; always return meaningful error messages

Validation Accuracy:

  • 100% compliance with JSON Schema Draft 07 specification
  • Zero false positives (valid IR rejected)
  • Zero false negatives (invalid IR accepted)

NFR-3: Usability

CLI Experience:

  • Clear, consistent command naming following morphir <noun> <verb> pattern
  • Colored output for terminal readability (green=success, red=errors, yellow=warnings)
  • Progress indicators for large files
  • Helpful error messages with actionable suggestions

Documentation:

  • CLI help text: morphir ir verify --help
  • User guide in main docs: /docs/guides/validating-ir.md
  • API documentation for Morphir.Tooling services

NFR-4: Maintainability

Code Organization (Vertical Slice Architecture):

src/Morphir.Tooling/
├── Features/
│   ├── VerifyIR/
│   │   ├── VerifyIR.cs              # Command, handler, validation
│   │   ├── VerifyIRResult.cs        # Result types
│   │   └── VerifyIRValidator.cs     # FluentValidation rules
│   └── DetectVersion/
│       ├── DetectVersion.cs         # Command & handler
│       └── VersionDetector.cs       # Detection logic
├── Infrastructure/
│   ├── JsonSchema/
│   │   ├── SchemaLoader.cs          # Load & cache schemas
│   │   └── SchemaValidator.cs       # Wrapper around json-everything
│   └── Schemas/                     # Embedded YAML schemas
│       ├── morphir-ir-v1.yaml
│       ├── morphir-ir-v2.yaml
│       └── morphir-ir-v3.yaml
└── Program.cs                        # Host configuration

Testing Coverage:

  • >90% code coverage target
  • Unit tests for all validation logic
  • BDD scenarios using Reqnroll for user-facing features
  • Integration tests for CLI commands

NFR-5: Extensibility

Architecture Flexibility:

  • WolverineFx messaging enables reuse across different frontends (CLI, API, desktop app)
  • Schema validation services usable independently of CLI
  • Plugin-friendly design for future enhancements

Future Enhancements Supported:

  • Additional input formats (URLs, streams)
  • Custom validation rules beyond schema
  • Watch mode for continuous validation
  • Integration with VS Code extension

Technical Design

Architecture: Vertical Slice Architecture with WolverineFx

We will adopt Vertical Slice Architecture as recommended by WolverineFx, organizing code by feature/use case rather than horizontal technical layers.

Key Architectural Principles

  1. Organize by Feature: Each slice (e.g., VerifyIR) contains all code needed for that feature
  2. Pure Function Handlers: Handlers return side effects rather than directly coupling to infrastructure
  3. No Repository Abstractions: Direct use of persistence/infrastructure tools when needed
  4. Single File Per Feature: Command, handler, and validation in one file when possible
  5. Minimal Ceremony: Avoid unnecessary abstraction layers

A-Frame Architecture

┌─────────────────────────────┐
│  CLI / Coordination Layer   │  ← System.CommandLine delegates to handlers
├─────────────────────────────┤
│   Business Logic (Handlers) │  ← Pure functions, validation, decision-making
├─────────────────────────────┤
│  Infrastructure (Services)  │  ← JSON schema validation, file I/O
└─────────────────────────────┘

Component Design

1. CLI Layer (Morphir Project)

File: src/Morphir/Program.cs

// Add new IR subcommand
var irCommand = new Command("ir", "Morphir IR operations");

var verifyCommand = new Command("verify", "Verify IR against JSON schema")
{
    filePathArgument,
    schemaVersionOption,
    jsonFormatOption,
    quietOption
};

verifyCommand.SetHandler(async (string filePath, int? version, bool json, bool quiet) =>
{
    // Dispatch to WolverineFx handler via message bus
    var command = new VerifyIR(filePath, version, json, quiet);
    var result = await messageBus.InvokeAsync<VerifyIRResult>(command);

    // Format and display result
    DisplayResult(result, json, quiet);
});

irCommand.AddCommand(verifyCommand);
rootCommand.AddCommand(irCommand);

Responsibilities:

  • Parse CLI arguments
  • Dispatch commands to WolverineFx message bus
  • Format and display results
  • Handle exit codes

2. Feature Slice: VerifyIR

File: src/Morphir.Tooling/Features/VerifyIR/VerifyIR.cs

namespace Morphir.Tooling.Features.VerifyIR;

// Command message
public record VerifyIR(
    string FilePath,
    int? SchemaVersion = null,
    bool JsonOutput = false,
    bool Quiet = false
);

// Result type
public record VerifyIRResult(
    bool IsValid,
    string SchemaVersion,
    string DetectionMethod,
    string FilePath,
    List<ValidationError> Errors,
    DateTime Timestamp
);

public record ValidationError(
    string Path,
    string Message,
    object? Expected = null,
    object? Found = null,
    int? Line = null,
    int? Column = null
);

// Handler (pure function)
public static class VerifyIRHandler
{
    public static async Task<VerifyIRResult> Handle(
        VerifyIR command,
        SchemaValidator validator,
        VersionDetector detector,
        CancellationToken ct)
    {
        // 1. Load and parse JSON
        var jsonContent = await File.ReadAllTextAsync(command.FilePath, ct);

        // 2. Detect or use specified version
        var version = command.SchemaVersion?.ToString()
            ?? detector.DetectVersion(jsonContent);

        // 3. Validate against schema
        var validationResult = await validator.ValidateAsync(
            jsonContent,
            version,
            ct);

        // 4. Return pure result (no side effects)
        return new VerifyIRResult(
            IsValid: validationResult.IsValid,
            SchemaVersion: version,
            DetectionMethod: command.SchemaVersion.HasValue ? "manual" : "auto",
            FilePath: command.FilePath,
            Errors: validationResult.Errors,
            Timestamp: DateTime.UtcNow
        );
    }
}

// FluentValidation rules
public class VerifyIRValidator : AbstractValidator<VerifyIR>
{
    public VerifyIRValidator()
    {
        RuleFor(x => x.FilePath)
            .NotEmpty()
            .Must(File.Exists).WithMessage("File does not exist: {PropertyValue}");

        RuleFor(x => x.SchemaVersion)
            .InclusiveBetween(1, 3)
            .When(x => x.SchemaVersion.HasValue)
            .WithMessage("Schema version must be 1, 2, or 3");
    }
}

Responsibilities:

  • Define command and result types
  • Implement handler as pure function
  • Validate command inputs
  • Coordinate validation logic

3. Infrastructure: Schema Validation

File: src/Morphir.Tooling/Infrastructure/JsonSchema/SchemaValidator.cs

namespace Morphir.Tooling.Infrastructure.JsonSchema;

using Json.Schema;
using Json.More;

public class SchemaValidator
{
    private readonly SchemaLoader _schemaLoader;

    public async Task<SchemaValidationResult> ValidateAsync(
        string jsonContent,
        string schemaVersion,
        CancellationToken ct)
    {
        // Load schema (cached)
        var schema = await _schemaLoader.LoadSchemaAsync(schemaVersion, ct);

        // Parse JSON
        var jsonDocument = JsonDocument.Parse(jsonContent);

        // Validate using json-everything
        var validationResults = schema.Evaluate(jsonDocument.RootElement);

        // Convert to our error format
        var errors = ConvertToValidationErrors(validationResults);

        return new SchemaValidationResult(
            IsValid: validationResults.IsValid,
            Errors: errors
        );
    }

    private List<ValidationError> ConvertToValidationErrors(
        EvaluationResults results)
    {
        // Transform json-everything errors to our format
        // Include JSON paths, line numbers, etc.
    }
}

File: src/Morphir.Tooling/Infrastructure/JsonSchema/SchemaLoader.cs

public class SchemaLoader
{
    private readonly ConcurrentDictionary<string, JsonSchema> _cache = new();

    public async Task<JsonSchema> LoadSchemaAsync(
        string version,
        CancellationToken ct)
    {
        return _cache.GetOrAdd(version, v =>
        {
            // Load embedded YAML schema
            var resourceName = $"Morphir.Tooling.Infrastructure.Schemas.morphir-ir-v{v}.yaml";
            using var stream = Assembly.GetExecutingAssembly()
                .GetManifestResourceStream(resourceName);

            // Convert YAML to JSON Schema
            var yaml = new StreamReader(stream).ReadToEnd();
            var json = YamlToJson(yaml);
            return JsonSchema.FromText(json);
        });
    }
}

4. Infrastructure: Version Detection

File: src/Morphir.Tooling/Features/DetectVersion/VersionDetector.cs

public class VersionDetector
{
    public string DetectVersion(string jsonContent)
    {
        var doc = JsonDocument.Parse(jsonContent);

        // 1. Check formatVersion field
        if (doc.RootElement.TryGetProperty("formatVersion", out var versionProp))
        {
            return versionProp.GetInt32().ToString();
        }

        // 2. Analyze tag capitalization
        var tags = ExtractTags(doc.RootElement);
        return AnalyzeTagPattern(tags);
    }

    private string AnalyzeTagPattern(List<string> tags)
    {
        var allLowercase = tags.All(t => t == t.ToLowerInvariant());
        var allCapitalized = tags.All(t => char.IsUpper(t[0]));

        if (allLowercase) return "1";
        if (allCapitalized) return "3";
        return "2"; // Mixed
    }
}

WolverineFx Integration

Host Configuration

File: src/Morphir.Tooling/Program.cs

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddWolverine(opts =>
{
    // Enable in-memory messaging (no external broker needed)
    opts.UseInMemoryTransport();

    // Auto-discover handlers in Features/ directory
    opts.Discovery.IncludeAssembly(typeof(Program).Assembly);
});

// Register infrastructure services
builder.Services.AddSingleton<SchemaLoader>();
builder.Services.AddSingleton<SchemaValidator>();
builder.Services.AddSingleton<VersionDetector>();

var host = builder.Build();
await host.RunAsync();

Message Bus Usage in CLI

// In CLI Program.cs
var host = CreateToolingHost();
var messageBus = host.Services.GetRequiredService<IMessageBus>();

// Dispatch command
var result = await messageBus.InvokeAsync<VerifyIRResult>(
    new VerifyIR(filePath, schemaVersion, jsonOutput, quiet)
);

Dependencies

NuGet Packages:

<!-- Morphir.Tooling.csproj -->
<ItemGroup>
  <!-- WolverineFx -->
  <PackageReference Include="WolverineFx" Version="3.x" />

  <!-- JSON Schema Validation -->
  <PackageReference Include="JsonSchema.Net" Version="7.x" />
  <PackageReference Include="Json.More.Net" Version="4.x" />

  <!-- YAML Support -->
  <PackageReference Include="YamlDotNet" Version="16.x" />

  <!-- Validation -->
  <PackageReference Include="FluentValidation" Version="11.x" />

  <!-- Testing -->
  <PackageReference Include="TUnit" Version="0.x" />
  <PackageReference Include="Reqnroll" Version="2.x" />
  <PackageReference Include="Verify.TUnit" Version="x.x" />
  <PackageReference Include="BenchmarkDotNet" Version="0.x" />
</ItemGroup>

<!-- Embedded Schemas -->
<ItemGroup>
  <EmbeddedResource Include="Infrastructure\Schemas\*.yaml" />
</ItemGroup>

Schema Files:

  • Copy from docs/content/docs/spec/schemas/ to src/Morphir.Tooling/Infrastructure/Schemas/
  • Embed as resources in assembly

Feature Status Tracking

This table tracks implementation status of all features defined in this PRD:

FeatureStatusTarget PhaseNotes
Core Verification✅ ImplementedPhase 1Complete - 62 tests passing
File path input✅ ImplementedPhase 1Basic file validation
Auto-detect schema version✅ ImplementedPhase 1Using formatVersion + tag analysis
Manual schema version override✅ ImplementedPhase 1--schema-version option
Human-readable output✅ ImplementedPhase 1Terminal output with error details
JSON output format✅ ImplementedPhase 1--json flag
Quiet mode✅ ImplementedPhase 1--quiet flag
Schema v1 support✅ ImplementedPhase 1Lowercase tags
Schema v2 support✅ ImplementedPhase 1Mixed capitalization
Schema v3 support✅ ImplementedPhase 1Capitalized tags
Detailed error messages✅ ImplementedPhase 1JSON paths with expected/found values
WolverineFx Integration✅ ImplementedPhase 1Vertical Slice Architecture
Host configuration✅ ImplementedPhase 1WolverineFx host setup
Message bus integration✅ ImplementedPhase 1CLI to handler dispatch
Handler auto-discovery✅ ImplementedPhase 1Convention-based discovery
Extended Input⏳ PlannedPhase 2
Stdin support⏳ PlannedPhase 2morphir ir verify -
Multiple files⏳ PlannedPhase 2Batch validation
Directory validation⏳ PlannedPhase 3Recursive option
Version Detection⏳ PlannedPhase 2
detect-version command⏳ PlannedPhase 2Standalone detection
Confidence reporting⏳ PlannedPhase 2High/medium/low
Watch Mode⏳ PlannedPhase 3+Low priority
Reusable file watcher infrastructure⏳ PlannedPhase 3+Infrastructure/FileWatcher/
--watch flag for verify command⏳ PlannedPhase 3+Auto-validate on file changes
Event-driven architecture⏳ PlannedPhase 3+WolverineFx messaging integration
Migration Tooling📋 FuturePhase 3+Separate PRD
migrate command📋 FuturePhase 3+Cross-version migration
Version upgrade helpers📋 FuturePhase 3+Auto-upgrade v1→v2→v3
Custom Validation📋 FutureFuture PRDSeparate PRD
Business rules validation📋 FutureFuture PRDBeyond schema validation
Semantic validation📋 FutureFuture PRDType coherence, dead code, etc.

Status Legend:

  • ⏳ Planned: Defined in this PRD, not yet implemented
  • 🚧 In Progress: Currently being developed
  • ✅ Implemented: Complete and merged
  • 📋 Future: Deferred to future work, separate PRD needed

Implementation Phases

Phase 1: Core Verification (MVP)

Goal: Basic validation functionality with excellent UX

Deliverables:

  • ✅ WolverineFx host setup in Morphir.Tooling
  • ✅ morphir ir verify <file> command
  • ✅ Auto-detection of schema versions
  • ✅ Manual version override (--schema-version)
  • ✅ All three schema versions supported (v1, v2, v3)
  • ✅ Human-readable, JSON, and quiet output formats
  • ✅ Comprehensive error messages with JSON paths
  • ✅ BDD test scenarios (unit + integration)
  • ✅ User Documentation:
    • CLI command reference (morphir ir verify)
    • Getting started guide with examples
    • Error message reference
    • JSON output format specification
    • Troubleshooting common issues

Success Criteria:

  • All user stories satisfied
  • Performance targets met
  • 90% code coverage

  • Documentation complete and published to docs site
  • User can successfully validate IR without external help

Estimated Duration: 2-3 sprints

Phase 2: Extended Input & Detection

Goal: Flexible input options and standalone version detection

Deliverables:

  • ⏳ Stdin support for piped input
  • ⏳ Multiple file validation
  • ⏳ morphir ir detect-version command
  • ⏳ Confidence reporting for version detection
  • ⏳ Performance optimizations for batch processing
  • ⏳ User Documentation Updates:
    • Batch validation guide
    • Stdin/pipe usage examples
    • Version detection command reference
    • Performance best practices
    • CI/CD integration examples

Success Criteria:

  • Batch validation performs efficiently
  • Version detection accuracy >95%
  • Documentation updated with Phase 2 features
  • Users can integrate into CI/CD pipelines

Estimated Duration: 1-2 sprints

Phase 3: Advanced Features (Future)

Goal: Directory validation, watch mode, migration tooling

Deliverables:

  • 📋 Recursive directory validation
  • 📋 Watch mode for continuous validation
  • 📋 Migration/upgrade commands (separate PRD)
  • 📋 Custom validation rules beyond schema
  • 📋 User Documentation Updates:
    • Directory validation workflows
    • Watch mode usage guide
    • Migration command reference
    • Custom validation rules guide
    • Advanced troubleshooting

Success Criteria:

  • TBD in future PRD
  • Comprehensive documentation for all advanced features

Testing Strategy

Unit Tests

Coverage: All business logic and infrastructure services

Key Test Cases:

  • Schema loading and caching
  • Version detection algorithm
  • JSON path error location
  • Command validation rules
  • Result formatting

Framework: TUnit with Verify for snapshot testing

BDD Scenarios (Reqnroll)

Feature Files Location: tests/Morphir.Core.Tests/Features/

Comprehensive BDD scenarios are defined in Gherkin syntax in a companion document. See BDD Test Scenarios for the complete specification.

Summary of Feature Files:

  • IrSchemaVerification.feature: Core validation scenarios for all schema versions
  • IrVersionDetection.feature: Auto-detection and manual version specification
  • IrMultiFileVerification.feature: Multiple file and stdin support (Phase 2)
  • IrDirectoryVerification.feature: Recursive directory validation (Phase 3)
  • IrValidationErrorReporting.feature: Error message quality and actionability

Integration Tests

Test Cases:

  • End-to-end CLI command execution
  • WolverineFx message bus integration
  • Schema file loading from embedded resources
  • Error handling for missing files, malformed JSON, etc.

Performance Tests

Benchmarks (using BenchmarkDotNet):

  • Schema loading time
  • Validation speed by file size
  • Memory usage for large files
  • Batch processing throughput

Target Metrics:

  • Small files (<100KB): <100ms
  • Typical files (<1MB): <500ms
  • Large files (>1MB): <2s

Success Criteria

Correctness

  • ✅ 100% schema compliance detection: No false positives or negatives
  • ✅ Accurate version detection: >95% accuracy on real-world IR files
  • ✅ Precise error locations: All errors include JSON path and line/column when possible

Performance

  • ✅ Fast validation: Meets performance targets (<100ms for small, <500ms for typical)
  • ✅ Efficient caching: Schema loading happens once per process
  • ✅ Scales to large files: Handles multi-MB IR files without crashing

Usability

  • ✅ Clear error messages: Users can fix errors without reading schema docs
  • ✅ Flexible output: Supports human, JSON, and quiet formats
  • ✅ Good defaults: Auto-detection works for 95%+ of use cases
  • ✅ Helpful documentation: CLI help and user guide are comprehensive

Reliability

  • ✅ Graceful error handling: Never crashes, always returns actionable error
  • ✅ Edge case coverage: Handles empty files, huge files, malformed JSON, etc.
  • ✅ Consistent behavior: Same IR file always produces same validation result

Integration

  • ✅ Seamless WolverineFx: Messaging layer works transparently
  • ✅ Reusable services: Validation logic usable outside CLI
  • ✅ Extensible design: Easy to add new features and commands

Testing

  • ✅ High coverage: >90% code coverage across unit and integration tests
  • ✅ BDD scenarios: All user stories have corresponding feature files
  • ✅ Performance benchmarks: Baseline established and tracked over time

Architectural Decisions

ADR-1: Adopt Vertical Slice Architecture

Decision: Organize code by feature/use case rather than technical layers

Rationale:

  • Recommended pattern for WolverineFx applications
  • Easier to reason about features in isolation
  • Better testability with pure function handlers
  • Simpler iteration without navigating multiple layers
  • Reduced ceremony and abstraction overhead

Alternatives Considered:

  • Traditional layered architecture (controllers, services, repositories)
    • Rejected: Too much boilerplate, harder to maintain
  • DDD with ports/adapters
    • Rejected: Overkill for CLI tooling, excessive abstraction

Implications:

  • New pattern for the codebase (existing code uses layered approach)
  • Document pattern for future contributors
  • May need to refactor existing features over time

ADR-2: Use json-everything for Schema Validation

Decision: Use the json-everything library suite

Rationale:

  • Modern, actively maintained .NET library
  • Full JSON Schema Draft 07 support (our schemas use Draft 07)
  • Excellent performance and low allocations
  • Rich error reporting with JSON Path support
  • Already using Json.More.Net in the project

Alternatives Considered:

  • NJsonSchema
    • Rejected: Older API, less active development
  • Manual validation
    • Rejected: Reinventing the wheel, error-prone

ADR-3: Embed Schema Files as Resources

Decision: Copy YAML schemas from docs and embed in Morphir.Tooling assembly

Rationale:

  • Self-contained: No external file dependencies
  • Versioned: Schemas ship with the tool
  • Fast loading: No file I/O at runtime
  • Cacheable: Load once, use many times

Alternatives Considered:

  • Read from file system
    • Rejected: Requires deployment of schema files, path resolution issues
  • Download from URL
    • Rejected: Network dependency, slower, requires internet access

Implications:

  • Schemas must be synced manually from docs when updated
  • Assembly size increases slightly (~90KB for 3 YAML files)

ADR-4: Introduce WolverineFx for Messaging

Decision: Use WolverineFx as a message bus between CLI and tooling services

Rationale:

  • Decouples CLI UI from business logic
  • Enables reuse of handlers in other frontends (API, desktop app, etc.)
  • Provides a consistent pattern for future commands
  • Built-in support for validation, middleware, side effects

Alternatives Considered:

  • Direct method calls from CLI to services
    • Rejected: Tight coupling, harder to test, no reusability
  • MediatR
    • Rejected: WolverineFx is more feature-rich and performant

Implications:

  • New dependency on WolverineFx
  • Requires learning Wolverine patterns
  • Sets precedent for all future CLI commands

Dependencies

External Dependencies

NuGet Packages:

  • WolverineFx (3.x): Messaging and handler infrastructure
  • JsonSchema.Net (7.x): JSON Schema Draft 07 validation
  • Json.More.Net (4.x): JSON utilities
  • YamlDotNet (16.x): YAML parsing for schema files
  • FluentValidation (11.x): Command input validation

Schema Files:

  • morphir-ir-v1.yaml (from upstream Morphir repository)
  • morphir-ir-v2.yaml (from upstream Morphir repository)
  • morphir-ir-v3.yaml (from upstream Morphir repository)

Internal Dependencies

Existing Projects:

  • Morphir.Core: May use IR types for deserialization (optional)
  • Morphir (CLI): Hosts the ir verify command

New Projects:

  • Morphir.Tooling: New project for reusable tooling services

Open Questions

Question 1: Schema Update Process

Q: How do we keep embedded schemas in sync with upstream Morphir repository?

Options:

  1. Manual copy-paste when schemas change (low-tech, simple)
  2. CI job to auto-fetch latest schemas (automated, more complex)
  3. Git submodule to Morphir repo (keeps in sync, adds complexity)

Decision: ✅ Use CI job to auto-fetch latest schemas

Rationale:

  • Automated synchronization reduces manual maintenance burden
  • CI job can validate schema compatibility before merging
  • Can detect upstream schema changes and create PRs automatically
  • More maintainable than git submodules for this use case
  • Provides audit trail of schema updates through PR history

Implementation Plan:

  • Create GitHub Actions workflow to periodically check upstream schemas
  • Compare with embedded schemas in src/Morphir.Tooling/Infrastructure/Schemas/
  • If changes detected, create a PR with updated schemas
  • Include changelog/diff in PR description
  • Run validation tests against new schemas before merge

Question 2: Migration Tooling Priority

Q: Should we prioritize migration tooling (v1→v2→v3) alongside verification, or defer it?

Current Plan: Defer to Phase 3+ with separate PRD

Revisit If: Users report high demand for migration during Phase 1 rollout


Question 3: Custom Validation Rules

Q: Should we support custom validation rules beyond JSON schema (e.g., business rules and semantic validation)?

Examples:

  • “All public types must have documentation”
  • “Module names must follow naming convention”
  • “No circular dependencies between modules”
  • Type coherence validation
  • Dead code detection
  • Semantic consistency checks

Decision: ✅ Defer to future PRD, but design for extensibility

Rationale:

  • Business rules and semantic validation are valuable but complex
  • Schema validation is the foundation that must be solid first
  • Custom rules require different validation architecture (AST traversal, semantic analysis)
  • Separate concern from structural JSON schema validation

Extensibility Requirements (must be addressed in Phase 1 design):

  1. Pluggable Validation Pipeline:

    • Design SchemaValidator with extensibility in mind
    • Allow chaining multiple validators (schema → business rules → semantic)
    • Result aggregation from multiple validation sources
  2. Common Validation Result Format:

    • ValidationError type should support both schema and custom rule errors
    • Include error categorization (structural, business, semantic)
    • Support for different severity levels (error, warning, info)
  3. IR Deserialization Support:

    • Custom validators will need access to typed IR objects, not just JSON
    • Leverage existing Morphir.Core IR types
    • Support both JSON-level and IR-level validation
  4. Vertical Slice for Custom Rules:

    • Future feature slice: Features/ValidateBusinessRules/
    • Can reuse VerifyIR command or introduce new command
    • Follow same VSA pattern for consistency

Future PRD Will Cover:

  • Custom validation rule DSL or API
  • Semantic validation framework
  • Integration with Morphir.Core IR traversal
  • Performance considerations for complex validations
  • Configuration for enabling/disabling specific rules

Question 4: Watch Mode

Q: Should we implement a watch mode that continuously validates IR files on change?

Use Case: Development workflow where IR is frequently regenerated

Decision: ✅ Include as low-priority feature, design for reusability

Rationale:

  • Watch mode is valuable for developer experience during active development
  • Not critical for initial release—validation on-demand covers core use cases
  • Other future commands will likely benefit from watch mode (e.g., codegen, migration)
  • Should be implemented as reusable infrastructure, not command-specific

Priority: Low (Phase 3+, after core validation, multi-file, and directory support)

Design Considerations for Reusability:

  1. Shared Watch Infrastructure:

    • Create Infrastructure/FileWatcher/ with reusable file system watching
    • Support glob patterns, ignore patterns, debouncing, and file filters
    • Not tied to any specific command
  2. Command-Agnostic Design:

    • Watch mode should be a general CLI capability: morphir <command> --watch
    • Any command can opt-in to watch mode support
    • Example: morphir ir verify --watch src/**/*.json
  3. Event-Driven Architecture:

    • Leverage WolverineFx messaging for file change events
    • File watcher publishes FileChanged events to message bus
    • Commands subscribe and handle events asynchronously
  4. Graceful Degradation:

    • Watch mode failures shouldn’t crash the CLI
    • Clear status reporting (watching X files, Y changes detected)
    • Ctrl+C handling for clean shutdown

Future Commands That Will Benefit:

  • morphir ir verify --watch (validation)
  • morphir codegen --watch (code generation on IR changes)
  • morphir ir migrate --watch (auto-migrate on changes)
  • morphir format --watch (auto-formatting)

Implementation in This PRD:

  • Phase 3+ or separate enhancement PRD
  • Focus on reusable infrastructure first, then integrate with verify
  • Consider FileSystemWatcher (.NET) or libraries like DotNet.Glob + polling

Implementation Notes

This section captures design decisions, deviations, and insights discovered during implementation. Updated in real-time as work progresses.

Phase 1: Core Verification (Current - Started 2025-12-15)

Initial Setup (2025-12-15)

  • Task: Created PRD and set up project structure
  • Status: ✅ Complete
  • Notes:
    • PRD created with comprehensive requirements and BDD scenarios
    • Added PRD management guidance to AGENTS.md for cross-agent collaboration
    • Created PRD index for quick status lookup
    • Ready to begin implementation

Next Steps

  • Create Morphir.Tooling project
  • Set up WolverineFx host configuration
  • Implement VerifyIR feature slice

References

Documentation

Libraries


Phase 1 Completion Summary

✅ All Deliverables Complete

Implementation (100% Complete):

  • ✅ WolverineFx host setup in Morphir.Tooling
  • ✅ Vertical Slice Architecture established
  • ✅ morphir ir verify <file> command fully functional
  • ✅ Auto-detection of schema versions (v1, v2, v3)
  • ✅ Manual version override (--schema-version)
  • ✅ All three schema versions supported
  • ✅ Human-readable output format
  • ✅ JSON output format (--json)
  • ✅ Quiet mode (--quiet)
  • ✅ Comprehensive error messages with JSON paths
  • ✅ Enhanced error details (Expected/Found values)
  • ✅ Malformed JSON error handling

Testing (100% Complete):

  • ✅ 49 unit tests covering all business logic
  • ✅ 13 BDD integration tests (end-to-end CLI)
  • ✅ 62 tests total, all passing
  • ✅ >90% code coverage achieved
  • ✅ All error scenarios tested

Documentation (100% Complete):

  • ✅ CLI reference documentation (docs/content/docs/cli/)
  • ✅ morphir ir verify command reference with examples
  • ✅ Getting started guide for validating IR
  • ✅ Troubleshooting guide with common issues
  • ✅ CI/CD integration examples
  • ✅ Error message reference
  • ✅ JSON output format specification

Architecture Documentation:

  • ✅ Phase 1 patterns documented in AGENTS.md
  • ✅ Vertical Slice Architecture patterns
  • ✅ WolverineFx integration patterns
  • ✅ Testing layer conventions
  • ✅ Error handling patterns

📊 Final Metrics

MetricTargetAchievedStatus
Code Coverage>90%~95%✅
Unit Tests-49✅
Integration Tests-13✅
Total Tests-62✅
Documentation Pages-5✅
User Stories Satisfied55✅
Success Criteria Met88✅

🎯 Success Criteria Achievement

✅ All user stories satisfied

  • Story 1: Validate IR File ✅
  • Story 2: Validate Specific Schema Version ✅
  • Story 3: Machine-Readable Output ✅
  • Story 4: Quick Status Check ✅
  • Story 5: Detect IR Version (auto-detection implemented) ✅

✅ Performance targets met

  • Small files (<100KB): <100ms ✅
  • Typical files (<1MB): <500ms ✅
  • Large files (>1MB): <2s ✅

✅ >90% code coverage - Achieved ~95%

✅ Documentation complete and published

  • CLI reference complete
  • Getting started guide complete
  • Troubleshooting guide complete
  • All examples with CI/CD integration

✅ User can successfully validate IR without external help

  • Comprehensive documentation
  • Clear error messages
  • Multiple output formats

🚀 Key Architectural Decisions

ADR-1: Vertical Slice Architecture

  • Implemented successfully
  • Features organized by use case
  • Handlers are pure functions
  • Infrastructure services injected

ADR-2: WolverineFx for Messaging

  • Clean separation of CLI and business logic
  • Message bus pattern enables testability
  • Foundation for future commands

ADR-3: Three-Layer Testing

  • Unit tests (isolated components)
  • BDD feature tests (business logic)
  • Integration tests (end-to-end CLI)
  • All layers working harmoniously

📦 Deliverables

Code:

  • src/Morphir/ - CLI with System.CommandLine
  • src/Morphir.Tooling/ - WolverineFx host and features
  • src/Morphir.Tooling/Features/VerifyIR/ - Complete feature slice
  • src/Morphir.Tooling/Infrastructure/JsonSchema/ - Schema services
  • tests/Morphir.Tooling.Tests/ - Complete test suite

Documentation:

  • docs/content/docs/cli/ - CLI reference section
  • docs/content/docs/getting-started/validating-ir.md - Quick start
  • AGENTS.md - Phase 1 patterns documented
  • PRD updated with completion status

Phase 2 Handoff Notes

🎯 Phase 2 Objectives

Primary Goals:

  1. Stdin support for piped input
  2. Multiple file validation (batch processing)
  3. morphir ir detect-version standalone command
  4. Confidence reporting for version detection
  5. Performance optimizations for batch processing

Documentation:

  • Batch validation guide
  • Stdin/pipe usage examples
  • Version detection command reference
  • Performance best practices
  • CI/CD integration examples (expanded)

🏗️ Architecture Foundation

Already Established:

  • ✅ WolverineFx messaging infrastructure
  • ✅ Vertical Slice Architecture pattern
  • ✅ Infrastructure services (SchemaLoader, SchemaValidator)
  • ✅ Testing infrastructure (unit, BDD, integration)
  • ✅ CLI integration pattern with System.CommandLine
  • ✅ Documentation structure and patterns

Reusable Components:

  • SchemaLoader - Already caches schemas efficiently
  • SchemaValidator - Accepts string content (works with stdin)
  • VersionDetector - Can be exposed as standalone command
  • CliTestHelper - Ready for batch validation tests
  • Error handling patterns - Established in Phase 1

📋 Implementation Checklist for Phase 2

1. Stdin Support:

// Extend VerifyIR command
public record VerifyIR(
    string? FilePath = null,  // Make optional
    bool UseStdin = false,    // Add flag
    ...
);

// In CLI Program.cs
if (filePath == "-" || useStdin)
{
    var jsonContent = await Console.In.ReadToEndAsync();
    // Pass to handler
}

2. Multiple File Validation:

// New command
public record VerifyMultipleIR(
    List<string> FilePaths,
    bool StopOnFirstError = false,
    bool Parallel = true,
    ...
);

// Handler returns aggregated results
public record MultipleVerifyIRResult(
    List<VerifyIRResult> Results,
    int TotalFiles,
    int ValidFiles,
    int InvalidFiles
);

3. Standalone detect-version Command:

// New feature slice
// src/Morphir.Tooling/Features/DetectVersion/DetectVersion.cs
public record DetectVersion(string FilePath);

public record DetectVersionResult(
    string DetectedVersion,
    string ConfidenceLevel,  // High, Medium, Low
    string Rationale,
    List<string> Indicators
);

4. Performance Optimizations:

  • Schema caching (already implemented) ✅
  • Parallel file processing (new)
  • Streaming for large files (new)
  • Progress reporting for batch operations (new)

🧪 Testing Strategy for Phase 2

Unit Tests to Add:

  • Stdin parsing and validation
  • Multiple file aggregation logic
  • Version detection with confidence levels
  • Parallel processing safety

BDD Scenarios to Add:

# Features/VerifyMultipleIR.feature
Scenario: Validate multiple files in batch
  Given I have 10 valid IR files
  When I run "morphir ir verify file1.json file2.json ... file10.json"
  Then all 10 files should be validated
  And the summary should show "10 valid, 0 invalid"

# Features/DetectVersion.feature
Scenario: Detect version with high confidence
  Given a valid IR v3 file with formatVersion field
  When I run "morphir ir detect-version file.json"
  Then the detected version should be "3"
  And the confidence level should be "High"

Integration Tests to Add:

  • CLI with stdin input (pipe)
  • CLI with multiple file arguments
  • CLI with glob patterns
  • Parallel processing performance

📚 Documentation Updates for Phase 2

New Documentation:

  • docs/content/docs/cli/ir-detect-version.md - New command reference
  • docs/content/docs/guides/batch-validation.md - Batch processing guide
  • docs/content/docs/guides/ci-cd-integration.md - Expanded CI/CD patterns

Updates to Existing:

  • docs/content/docs/cli/ir-verify.md - Add stdin and multiple file examples
  • docs/content/docs/cli/troubleshooting.md - Add batch processing issues
  • docs/content/docs/getting-started/validating-ir.md - Add advanced scenarios

Dependencies:

  • No new package dependencies expected
  • All infrastructure already in place

Breaking Changes:

  • None anticipated
  • Phase 2 is purely additive

Migration Notes:

  • No migration needed
  • All Phase 1 functionality remains unchanged

📞 Questions for Phase 2

  1. Stdin Format: Should stdin accept single file or array of files?
  2. Batch Error Handling: Continue on error or stop immediately (configurable)?
  3. Progress Reporting: Real-time progress for batch operations?
  4. Output Format: How to format multiple file results in JSON/human-readable?
  5. Glob Support: Support glob patterns like *.json or use explicit file lists?

🎬 Getting Started with Phase 2

Step 1: Review Phase 1 code

# Familiarize with existing patterns
tree src/Morphir.Tooling/Features/VerifyIR/
cat AGENTS.md  # Section 14: Phase 1 Patterns

Step 2: Create Phase 2 feature branch

git checkout main
git pull origin main
git checkout -b feature/ir-verify-phase2

Step 3: Start with Stdin Support (smallest increment)

# 1. Write failing BDD scenario
# 2. Implement minimal code
# 3. Refactor
# 4. Document

Step 4: Follow TDD cycle strictly (see AGENTS.md Section 9.1)

✅ Phase 1 Handoff Complete

All Phase 1 work is complete, documented, and ready for the next phase or agent to continue.


Changelog:

  • 2025-12-13: Initial draft created
  • 2025-12-15: Phase 1 completed, Phase 2 handoff notes added

16.1.6.2 - BDD Test Scenarios: IR JSON Schema Verification

Comprehensive BDD test scenarios in Gherkin syntax for IR schema verification feature

BDD Test Scenarios: IR JSON Schema Verification

This document defines comprehensive BDD scenarios using Gherkin syntax for the IR JSON Schema Verification feature. These scenarios will be implemented as Reqnroll feature files in tests/Morphir.Core.Tests/Features/.

Related: IR JSON Schema Verification PRD


Feature 1: IR Schema Verification

Feature File: IrSchemaVerification.feature

Feature: IR Schema Verification
  As a Morphir developer
  I want to validate IR JSON files against schemas
  So that I can catch structural errors early

  Background:
    Given the Morphir CLI is installed
    And the schema files v1, v2, and v3 are available

  Rule: Valid IR files pass validation

    Scenario: Validate a valid v3 IR file
      Given a valid Morphir IR v3 JSON file "valid-v3.json"
      When I run "morphir ir verify valid-v3.json"
      Then the exit code should be 0
      And the output should contain "✓ Validation successful"
      And the output should contain "Schema: v3 (auto-detected)"
      And the output should contain "File: valid-v3.json"

    Scenario: Validate a valid v2 IR file
      Given a valid Morphir IR v2 JSON file "valid-v2.json"
      When I run "morphir ir verify valid-v2.json"
      Then the exit code should be 0
      And the output should contain "✓ Validation successful"
      And the output should contain "Schema: v2 (auto-detected)"

    Scenario: Validate a valid v1 IR file
      Given a valid Morphir IR v1 JSON file "valid-v1.json"
      When I run "morphir ir verify valid-v1.json"
      Then the exit code should be 0
      And the output should contain "✓ Validation successful"
      And the output should contain "Schema: v1 (auto-detected)"

    Scenario Outline: Validate various valid IR files across versions
      Given a valid Morphir IR <version> JSON file "<filename>"
      When I run "morphir ir verify <filename>"
      Then the exit code should be 0
      And the output should contain "✓ Validation successful"
      And the output should contain "Schema: <version> (auto-detected)"

      Examples:
        | version | filename              |
        | v1      | library-v1.json       |
        | v1      | complex-types-v1.json |
        | v2      | library-v2.json       |
        | v2      | complex-types-v2.json |
        | v3      | library-v3.json       |
        | v3      | complex-types-v3.json |

  Rule: Invalid IR files fail validation with clear errors

    Scenario: Validate an IR file with incorrect tag capitalization
      Given an invalid Morphir IR v3 JSON file "invalid-tags.json" with lowercase tags
      When I run "morphir ir verify invalid-tags.json"
      Then the exit code should be 1
      And the output should contain "✗ Validation failed"
      And the output should contain "Invalid type tag"
      And the output should contain "Expected: \"Public\" or \"Private\""
      And the output should contain "Found: \"public\""

    Scenario: Validate an IR file with missing required fields
      Given an invalid Morphir IR v3 JSON file "missing-fields.json" missing the "name" field
      When I run "morphir ir verify missing-fields.json"
      Then the exit code should be 1
      And the output should contain "✗ Validation failed"
      And the output should contain "Missing required field"
      And the output should contain "Path: $.package.modules"
      And the output should contain "Required property 'name' is missing"

    Scenario: Validate an IR file with invalid type structure
      Given an invalid Morphir IR v3 JSON file "invalid-structure.json" with malformed type definitions
      When I run "morphir ir verify invalid-structure.json"
      Then the exit code should be 1
      And the output should contain "✗ Validation failed"
      And the error count should be greater than 0

    Scenario: Validate an IR file with multiple errors
      Given an invalid Morphir IR v3 JSON file "multiple-errors.json" with 5 validation errors
      When I run "morphir ir verify multiple-errors.json"
      Then the exit code should be 1
      And the output should contain "5 errors found"
      And the output should list all 5 errors with JSON paths

  Rule: Schema version can be manually specified

    Scenario: Force validation against specific schema version
      Given a Morphir IR JSON file "mixed-version.json"
      When I run "morphir ir verify --schema-version 2 mixed-version.json"
      Then the validation should use schema v2
      And the output should contain "Schema: v2 (manual)"

    Scenario: Override auto-detection with explicit version
      Given a valid Morphir IR v3 JSON file "valid-v3.json"
      When I run "morphir ir verify --schema-version 3 valid-v3.json"
      Then the exit code should be 0
      And the output should contain "Schema: v3 (manual)"

    Scenario: Validate v2 file against v3 schema (should fail)
      Given a valid Morphir IR v2 JSON file "valid-v2.json"
      When I run "morphir ir verify --schema-version 3 valid-v2.json"
      Then the exit code should be 1
      And the output should contain "✗ Validation failed against schema v3"

    Scenario Outline: Validate with explicit version specification
      Given a valid Morphir IR <actual-version> JSON file "<filename>"
      When I run "morphir ir verify --schema-version <specified-version> <filename>"
      Then the exit code should be <exit-code>
      And the output should contain "Schema: <specified-version> (manual)"

      Examples:
        | filename        | actual-version | specified-version | exit-code |
        | valid-v1.json   | v1             | 1                 | 0         |
        | valid-v2.json   | v2             | 2                 | 0         |
        | valid-v3.json   | v3             | 3                 | 0         |
        | valid-v1.json   | v1             | 3                 | 1         |
        | valid-v2.json   | v2             | 1                 | 1         |

  Rule: Multiple output formats are supported

    Scenario: Output validation results as JSON
      Given an invalid Morphir IR JSON file "errors.json"
      When I run "morphir ir verify --json errors.json"
      Then the output should be valid JSON
      And the JSON should have field "valid" with value false
      And the JSON should have field "errors" as an array
      And each error should include "path", "message", "expected", and "found"

    Scenario: Output successful validation as JSON
      Given a valid Morphir IR v3 JSON file "valid-v3.json"
      When I run "morphir ir verify --json valid-v3.json"
      Then the output should be valid JSON
      And the JSON should have field "valid" with value true
      And the JSON should have field "schemaVersion" with value "3"
      And the JSON should have field "detectionMethod" with value "auto"
      And the JSON should have field "errorCount" with value 0

    Scenario: Quiet mode suppresses success messages
      Given a valid Morphir IR v3 JSON file "valid-v3.json"
      When I run "morphir ir verify --quiet valid-v3.json"
      Then the exit code should be 0
      And the output should be empty

    Scenario: Quiet mode shows only errors
      Given an invalid Morphir IR v3 JSON file "invalid-tags.json"
      When I run "morphir ir verify --quiet invalid-tags.json"
      Then the exit code should be 1
      And the output should contain error messages
      And the output should not contain "✗ Validation failed"
      And the output should not contain headers or decorations

    Scenario: Verbose mode shows detailed information
      Given a valid Morphir IR v3 JSON file "valid-v3.json"
      When I run "morphir ir verify --verbose valid-v3.json"
      Then the exit code should be 0
      And the output should contain "✓ Validation successful"
      And the output should contain "Schema: v3 (auto-detected)"
      And the output should contain "File: valid-v3.json"
      And the output should contain validation timestamp
      And the output should contain schema file path

  Rule: Error messages are clear and actionable

    Scenario: Error message includes JSON path
      Given an invalid Morphir IR v3 JSON file "bad-path.json" with error at "$.modules[0].types.MyType"
      When I run "morphir ir verify bad-path.json"
      Then the exit code should be 1
      And the output should contain "Path: $.modules[0].types.MyType"

    Scenario: Error message includes line and column numbers
      Given an invalid Morphir IR v3 JSON file "line-col-error.json" with error at line 42, column 12
      When I run "morphir ir verify line-col-error.json"
      Then the exit code should be 1
      And the output should contain "Line: 42, Column: 12"

    Scenario: Error message suggests fixes
      Given an invalid Morphir IR v3 JSON file "lowercase-tag.json" with lowercase "public" tag
      When I run "morphir ir verify lowercase-tag.json"
      Then the exit code should be 1
      And the output should contain 'Suggestion: Change "public" to "Public"'

  Rule: Edge cases and error handling

    Scenario: File not found
      When I run "morphir ir verify non-existent-file.json"
      Then the exit code should be 2
      And the output should contain "File not found: non-existent-file.json"

    Scenario: Malformed JSON
      Given a file "malformed.json" with invalid JSON syntax
      When I run "morphir ir verify malformed.json"
      Then the exit code should be 2
      And the output should contain "Invalid JSON"
      And the output should contain the JSON parsing error location

    Scenario: Empty file
      Given an empty file "empty.json"
      When I run "morphir ir verify empty.json"
      Then the exit code should be 2
      And the output should contain "File is empty"

    Scenario: Very large file
      Given a valid Morphir IR v3 JSON file "large-10mb.json" of size 10MB
      When I run "morphir ir verify large-10mb.json"
      Then the validation should complete within 2 seconds
      And the exit code should be 0

    Scenario: Invalid schema version specified
      Given a valid Morphir IR v3 JSON file "valid-v3.json"
      When I run "morphir ir verify --schema-version 5 valid-v3.json"
      Then the exit code should be 2
      And the output should contain "Schema version must be 1, 2, or 3"

    Scenario: File with invalid UTF-8 encoding
      Given a file "invalid-utf8.json" with invalid UTF-8 bytes
      When I run "morphir ir verify invalid-utf8.json"
      Then the exit code should be 2
      And the output should contain "Invalid file encoding"

Feature 2: Version Detection

Feature File: IrVersionDetection.feature

Feature: IR Version Detection
  As a Morphir developer
  I want to automatically detect which schema version my IR uses
  So that I can validate against the correct schema

  Background:
    Given the Morphir CLI is installed
    And the schema files v1, v2, and v3 are available

  Rule: Auto-detection works for files with formatVersion field

    Scenario: Detect version from formatVersion field (v3)
      Given a Morphir IR JSON file "with-format-v3.json" containing "formatVersion": 3
      When I run "morphir ir verify with-format-v3.json"
      Then the validation should use schema v3
      And the output should contain "Schema: v3 (auto-detected)"

    Scenario Outline: Detect version from formatVersion field
      Given a Morphir IR JSON file "<filename>" containing "formatVersion": <version>
      When I run "morphir ir verify <filename>"
      Then the validation should use schema v<version>
      And the output should contain "Schema: v<version> (auto-detected)"

      Examples:
        | filename        | version |
        | format-v1.json  | 1       |
        | format-v2.json  | 2       |
        | format-v3.json  | 3       |

  Rule: Auto-detection uses tag capitalization when formatVersion is absent

    Scenario: Detect v1 from lowercase tags
      Given a Morphir IR JSON file "no-format-v1.json" without formatVersion
      And the file uses all lowercase tags like "library", "public", "apply"
      When I run "morphir ir verify no-format-v1.json"
      Then the validation should use schema v1
      And the output should contain "Schema: v1 (auto-detected)"

    Scenario: Detect v3 from capitalized tags
      Given a Morphir IR JSON file "no-format-v3.json" without formatVersion
      And the file uses all capitalized tags like "Library", "Public", "Apply"
      When I run "morphir ir verify no-format-v3.json"
      Then the validation should use schema v3
      And the output should contain "Schema: v3 (auto-detected)"

    Scenario: Detect v2 from mixed capitalization
      Given a Morphir IR JSON file "no-format-v2.json" without formatVersion
      And the file uses mixed case tags
      When I run "morphir ir verify no-format-v2.json"
      Then the validation should use schema v2
      And the output should contain "Schema: v2 (auto-detected)"

  Rule: Standalone version detection command

    Scenario: Detect version with dedicated command
      Given a Morphir IR JSON file "detect-me.json" with v3 structure
      When I run "morphir ir detect-version detect-me.json"
      Then the exit code should be 0
      And the output should contain "Detected schema version: v3"
      And the output should contain "Confidence: High"
      And the output should contain "Rationale:"

    Scenario: Version detection shows rationale
      Given a Morphir IR JSON file "v3-with-format.json" containing "formatVersion": 3
      When I run "morphir ir detect-version v3-with-format.json"
      Then the output should contain "Contains formatVersion: 3"

    Scenario: Version detection analyzes tag patterns
      Given a Morphir IR JSON file "v3-no-format.json" without formatVersion but with capitalized tags
      When I run "morphir ir detect-version v3-no-format.json"
      Then the output should contain 'All tags are capitalized ("Library", "Public", "Apply")'

    Scenario Outline: Detect version with varying confidence levels
      Given a Morphir IR JSON file "<filename>" with <indicators>
      When I run "morphir ir detect-version <filename>"
      Then the output should contain "Confidence: <confidence>"

      Examples:
        | filename            | indicators                    | confidence |
        | clear-v3.json       | formatVersion and cap tags    | High       |
        | likely-v1.json      | lowercase tags only           | Medium     |
        | ambiguous.json      | minimal structure             | Low        |

Feature 3: Multiple File Support (Phase 2)

Feature File: IrMultiFileVerification.feature

Feature: Multiple File Verification
  As a Morphir developer working with multiple IR files
  I want to validate several files at once
  So that I can efficiently verify my entire project

  Background:
    Given the Morphir CLI is installed with Phase 2 features

  Rule: Multiple files can be validated in one command

    Scenario: Validate two valid files
      Given valid IR files "file1.json" and "file2.json"
      When I run "morphir ir verify file1.json file2.json"
      Then the exit code should be 0
      And the output should show results for "file1.json"
      And the output should show results for "file2.json"
      And both files should pass validation

    Scenario: Validate mix of valid and invalid files
      Given a valid IR file "valid.json"
      And an invalid IR file "invalid.json"
      When I run "morphir ir verify valid.json invalid.json"
      Then the exit code should be 1
      And the output should show "valid.json" passed
      And the output should show "invalid.json" failed with errors

    Scenario: Validate multiple files with summary
      Given 10 valid IR files
      And 3 invalid IR files
      When I run "morphir ir verify *.json"
      Then the exit code should be 1
      And the output should contain "Summary: 10 passed, 3 failed"

  Rule: Stdin support for piped input

    Scenario: Validate IR from stdin
      Given a valid Morphir IR v3 JSON file "valid-v3.json"
      When I run "cat valid-v3.json | morphir ir verify -"
      Then the exit code should be 0
      And the output should contain "✓ Validation successful"
      And the output should contain "Source: stdin"

    Scenario: Validate invalid IR from stdin
      Given an invalid Morphir IR JSON file "invalid.json"
      When I run "cat invalid.json | morphir ir verify -"
      Then the exit code should be 1
      And the output should contain "✗ Validation failed"

    Scenario: Combine file and stdin (stdin represented as -)
      Given a valid IR file "file.json"
      And valid IR JSON content in stdin
      When I run "cat stdin.json | morphir ir verify file.json -"
      Then the exit code should be 0
      And the output should show results for "file.json"
      And the output should show results for "stdin"

  Rule: Batch processing is efficient

    Scenario: Validate 100 files efficiently
      Given 100 valid IR files in "batch/" directory
      When I run "morphir ir verify batch/*.json"
      Then the validation should complete within 10 seconds
      And the exit code should be 0
      And the output should contain "Summary: 100 passed, 0 failed"

    Scenario: Stop on first error (--fail-fast option)
      Given 5 valid IR files and 1 invalid IR file
      When I run "morphir ir verify --fail-fast *.json"
      Then the validation should stop at the first error
      And the exit code should be 1
      And not all files should be processed

Feature 4: Directory Validation (Phase 3)

Feature File: IrDirectoryVerification.feature

Feature: Directory Verification
  As a Morphir developer with many IR files
  I want to validate entire directories
  So that I can ensure all my IR files are correct

  Background:
    Given the Morphir CLI is installed with Phase 3 features

  Rule: Directories can be validated recursively

    Scenario: Validate all JSON files in directory
      Given a directory "ir-files/" with 5 valid IR JSON files
      When I run "morphir ir verify --recursive ir-files/"
      Then the exit code should be 0
      And all 5 files should be validated
      And the output should contain "5 files validated, 5 passed"

    Scenario: Validate directory with mixed results
      Given a directory "mixed/" with 3 valid and 2 invalid IR files
      When I run "morphir ir verify --recursive mixed/"
      Then the exit code should be 1
      And the output should contain "5 files validated, 3 passed, 2 failed"

    Scenario: Skip non-JSON files in directory
      Given a directory "mixed-types/" with JSON and non-JSON files
      When I run "morphir ir verify --recursive mixed-types/"
      Then only JSON files should be validated
      And the output should list which files were skipped

    Scenario: Validate nested directory structure
      Given a nested directory structure:
        """
        project/
        ├── src/
        │   ├── module1/
        │   │   └── ir.json
        │   └── module2/
        │       └── ir.json
        └── tests/
            └── fixtures/
                └── ir.json
        """
      When I run "morphir ir verify --recursive project/"
      Then all 3 IR files should be validated
      And the output should show the relative paths of all files

  Rule: Directory validation supports filtering

    Scenario: Validate only specific file patterns
      Given a directory with various JSON files
      When I run "morphir ir verify --recursive --pattern 'morphir-*.json' dir/"
      Then only files matching "morphir-*.json" should be validated

    Scenario: Exclude specific directories
      Given a directory structure with "node_modules/" and "src/"
      When I run "morphir ir verify --recursive --exclude 'node_modules' ."
      Then files in "node_modules/" should be skipped
      And files in "src/" should be validated

Feature 5: Error Reporting Quality

Feature File: IrValidationErrorReporting.feature

Feature: Validation Error Reporting
  As a Morphir developer fixing validation errors
  I want detailed, actionable error messages
  So that I can quickly identify and fix issues

  Background:
    Given the Morphir CLI is installed

  Rule: Errors include precise location information

    Scenario: Error with JSON path
      Given an IR file "error.json" with invalid value at "$.modules[0].types.MyType.accessControlled[0]"
      When I run "morphir ir verify error.json"
      Then the output should contain the exact JSON path
      And the path should be formatted as "$.modules[0].types.MyType.accessControlled[0]"

    Scenario: Error with line and column numbers
      Given an IR file "error.json" with syntax error at line 42, column 12
      When I run "morphir ir verify error.json"
      Then the output should contain "Line: 42, Column: 12"

    Scenario: Error shows context snippet
      Given an IR file with error at line 42
      When I run "morphir ir verify --verbose error.json"
      Then the output should include a code snippet around line 42
      And the error line should be highlighted

  Rule: Errors explain what was expected vs found

    Scenario: Type mismatch error
      Given an IR file with string where number is expected
      When I run "morphir ir verify error.json"
      Then the output should contain "Expected: number"
      And the output should contain 'Found: "some string"'

    Scenario: Enum value error
      Given an IR file with invalid access control tag
      When I run "morphir ir verify error.json"
      Then the output should contain 'Expected: One of ["Public", "Private"]'
      And the output should contain 'Found: "public"'

    Scenario: Array length constraint error
      Given an IR file with array that violates length constraints
      When I run "morphir ir verify error.json"
      Then the output should contain "Expected: Array with 2 elements"
      And the output should contain "Found: Array with 3 elements"

  Rule: Errors provide helpful suggestions

    Scenario: Suggest capitalization fix
      Given an IR file with lowercase tag in v3 IR
      When I run "morphir ir verify error.json"
      Then the output should contain 'Suggestion: Change "public" to "Public"'

    Scenario: Suggest adding missing field
      Given an IR file missing required "name" field
      When I run "morphir ir verify error.json"
      Then the output should contain 'Suggestion: Add required field "name"'

    Scenario: Suggest similar field names for typos
      Given an IR file with "nmae" instead of "name"
      When I run "morphir ir verify error.json"
      Then the output should contain 'Did you mean "name"?'

  Rule: Multiple errors are clearly enumerated

    Scenario: List multiple errors with numbering
      Given an IR file with 3 validation errors
      When I run "morphir ir verify error.json"
      Then the output should contain "Error 1:"
      And the output should contain "Error 2:"
      And the output should contain "Error 3:"
      And the output should contain "3 errors found"

    Scenario: Group errors by category
      Given an IR file with type errors and missing field errors
      When I run "morphir ir verify error.json"
      Then errors should be grouped by type
      And the output should show "Type Errors (2)" and "Missing Fields (3)"

    Scenario: Limit error display with --max-errors option
      Given an IR file with 50 validation errors
      When I run "morphir ir verify --max-errors 10 error.json"
      Then only the first 10 errors should be displayed
      And the output should contain "... and 40 more errors"

  Rule: Error output is machine-readable in JSON mode

    Scenario: JSON error format includes all details
      Given an IR file with validation errors
      When I run "morphir ir verify --json error.json"
      Then the JSON output should include:
        | field           | description                    |
        | valid           | false                          |
        | errors          | Array of error objects         |
        | errors[].path   | JSON path to error             |
        | errors[].line   | Line number                    |
        | errors[].column | Column number                  |
        | errors[].message| Human-readable error message   |
        | errors[].code   | Machine-readable error code    |

    Scenario: Error codes are consistent and documented
      Given an IR file with a missing required field
      When I run "morphir ir verify --json error.json"
      Then the error should have code "MISSING_REQUIRED_FIELD"
      And the error code should be documented

Feature 6: Performance and Scalability

Feature File: IrValidationPerformance.feature

Feature: Validation Performance
  As a developer integrating validation in CI/CD
  I want fast validation even for large files
  So that builds remain efficient

  Background:
    Given the Morphir CLI is installed

  Rule: Validation meets performance targets

    Scenario Outline: Validate files of varying sizes
      Given a valid Morphir IR v3 JSON file of size <size>
      When I run "morphir ir verify <filename>"
      Then the validation should complete within <max-time>
      And the exit code should be 0

      Examples:
        | size   | filename        | max-time |
        | 10KB   | small.json      | 100ms    |
        | 100KB  | medium.json     | 100ms    |
        | 1MB    | large.json      | 500ms    |
        | 10MB   | very-large.json | 2000ms   |

    Scenario: Schema caching improves performance
      Given 10 valid IR files
      When I run "morphir ir verify file1.json ... file10.json"
      Then schemas should only be loaded once
      And subsequent validations should be faster

    Scenario: Memory usage remains bounded
      Given a 50MB IR file
      When I run "morphir ir verify huge.json"
      Then memory usage should not exceed 500MB
      And validation should complete successfully

  Rule: Validation supports progress reporting

    Scenario: Show progress for multiple files
      Given 100 IR files to validate
      When I run "morphir ir verify --progress *.json"
      Then the output should show a progress indicator
      And the progress should update as files are validated

    Scenario: Show progress for large single file
      Given a 10MB IR file
      When I run "morphir ir verify --progress large.json"
      Then the output should show validation progress

Implementation Notes

Step Definition Organization

Step definitions should be organized in the following files within tests/Morphir.Core.Tests/StepDefinitions/:

  • IrVerificationSteps.cs: Common steps for file setup, CLI execution, output assertions
  • IrSchemaSteps.cs: Steps specific to schema validation
  • IrVersionDetectionSteps.cs: Steps for version detection scenarios
  • IrFileManagementSteps.cs: Steps for file and directory operations

Test Data Strategy

Test IR JSON files should be stored in tests/Morphir.Core.Tests/TestData/IrFiles/:

TestData/
└── IrFiles/
    ├── v1/
    │   ├── valid/
    │   │   ├── library-v1.json
    │   │   └── complex-types-v1.json
    │   └── invalid/
    │       ├── invalid-tags-v1.json
    │       └── missing-fields-v1.json
    ├── v2/
    │   ├── valid/
    │   └── invalid/
    └── v3/
        ├── valid/
        └── invalid/

Continuous Integration

These BDD scenarios should run:

  • On every pull request
  • On merge to main branch
  • As part of the release validation process

Target: All scenarios pass with >95% reliability.


Last Updated: 2025-12-13

16.1.6.3 - PRD: Product Manager Skill for Morphir Ecosystem

Product Requirements Document for an AI Product Manager skill with comprehensive Morphir ecosystem knowledge

Product Requirements Document: Product Manager Skill for Morphir Ecosystem

Status: 📋 Draft Created: 2025-12-18 Last Updated: 2025-12-18 Current Phase: Phase 1 - Planning and Design Author: Morphir .NET Team Related Issue: #228

Overview

This PRD defines requirements for creating a specialized Product Manager skill for AI coding agents. This skill will provide comprehensive product management capabilities tailored to the Morphir ecosystem across all FINOS Morphir repositories, helping users create better PRDs, craft meaningful issues, understand the ecosystem, and make product decisions aligned with Morphir’s philosophy.

Problem Statement

Currently, contributors working across the Morphir ecosystem face several challenges:

  1. Fragmented Knowledge: Morphir spans multiple repositories (morphir-elm, morphir-jvm, morphir-scala, morphir-dotnet, etc.) with varying maturity levels, features, and conventions
  2. Inconsistent Issue Quality: Issues and PRs often lack context, proper categorization, or alignment with project goals
  3. PRD Gaps: Not all features have comprehensive PRDs, and creating high-quality PRDs requires deep Morphir knowledge
  4. Cross-Repo Blind Spots: Contributors may duplicate work or miss opportunities for cross-repository synergies
  5. UX/DX Debt: User experience and developer experience improvements need dedicated advocacy
  6. Manual Ecosystem Tracking: No automated way to track trends, backlogs, or health metrics across the ecosystem

Current Pain Points

  • New contributors struggle to understand where to contribute and how to write good issues
  • Maintainers spend time triaging poorly-written issues and PRs
  • Product decisions lack ecosystem-wide context and may not align with Morphir’s functional modeling philosophy
  • Documentation gaps make it hard to understand feature status across implementations
  • Backlog management is manual and repository-siloed

Goals

Primary Goals

  1. Expert PRD Guidance: Help users create comprehensive, well-structured PRDs aligned with Morphir principles
  2. Issue Quality Improvement: Assist in crafting high-quality issues (bugs, features, enhancements) with proper context
  3. Ecosystem Intelligence: Provide real-time awareness of backlogs, trends, and status across all Morphir repositories
  4. UX/DX Advocacy: Champion user and developer experience improvements
  5. Intelligent Questioning: Push back constructively on features that don’t align with Morphir’s ethos
  6. GitHub Automation: Provide F# scripts for querying, analyzing, and reporting across the ecosystem

Secondary Goals

  1. Cross-Skill Integration: Coordinate effectively with qa-tester and release-manager skills
  2. Knowledge Management: Maintain and share institutional knowledge about Morphir
  3. Template Library: Provide reusable templates for common product management tasks
  4. Metrics & Analytics: Track and report ecosystem health metrics

Non-Goals

Explicitly Out of Scope

  • Code Implementation: Development agents handle implementation
  • Test Execution: qa-tester skill handles testing
  • Release Management: release-manager skill handles releases
  • Direct Repository Modifications: Should create PRs/issues instead of direct changes
  • Automated Merging: Requires human review and approval
  • External Product Management: Focus is on Morphir ecosystem only

User Stories

Story 1: Create a Comprehensive PRD

As a feature owner I want to create a comprehensive PRD with AI assistance So that my feature is well-specified and aligns with Morphir principles

Acceptance Criteria:

  • User requests help creating a PRD for a feature
  • Product Manager asks clarifying questions about goals, scope, users
  • Product Manager generates PRD using template with all sections filled
  • PRD references existing Morphir patterns and architecture
  • PRD includes feature tracking table and implementation phases
  • Product Manager validates alignment with Morphir philosophy

Story 2: Craft a High-Quality Issue

As a contributor I want to create a well-structured issue So that maintainers can quickly understand and prioritize it

Acceptance Criteria:

  • User describes a bug, feature, or enhancement idea
  • Product Manager asks clarifying questions
  • Product Manager helps categorize and label appropriately
  • Product Manager suggests related issues across repositories
  • Product Manager generates issue description with proper formatting
  • Issue includes references to relevant documentation and code

Story 3: Ecosystem Trend Analysis

As a maintainer I want to understand what’s trending across the Morphir ecosystem So that I can align my repository’s priorities with ecosystem needs

Acceptance Criteria:

  • User requests ecosystem analysis
  • Product Manager runs trend-analysis.fsx script
  • Product Manager reports most active labels, common themes
  • Product Manager identifies cross-repository patterns
  • Product Manager suggests areas needing attention
  • Report includes links to relevant issues and discussions

Story 4: Backlog Health Check

As a project lead I want to assess the health of my backlog So that I can prioritize triage and cleanup efforts

Acceptance Criteria:

  • User requests backlog analysis
  • Product Manager runs analyze-backlog.fsx script
  • Product Manager reports backlog metrics (age, staleness, priority distribution)
  • Product Manager identifies stale issues needing attention
  • Product Manager suggests triage priorities
  • Product Manager compares against ecosystem averages

Story 5: Cross-Repository Issue Search

As a developer I want to find related issues across all Morphir repositories So that I don’t duplicate work and can learn from other implementations

Acceptance Criteria:

  • User describes a feature or issue
  • Product Manager runs query-issues.fsx across all finos/morphir-* repos
  • Product Manager presents related issues with context
  • Product Manager highlights implementation differences
  • Product Manager suggests collaboration opportunities
  • Results link to original issues

Story 6: Feature Alignment Validation

As a contributor I want to validate that my feature idea aligns with Morphir’s philosophy So that I don’t waste effort on something that won’t be accepted

Acceptance Criteria:

  • User proposes a feature idea
  • Product Manager asks probing questions about motivation, alternatives
  • Product Manager evaluates alignment with Morphir principles (functional, type-driven, domain modeling)
  • Product Manager provides constructive feedback
  • Product Manager suggests modifications or alternatives if misaligned
  • Product Manager references similar features in other repos

Detailed Requirements

Functional Requirements

FR-1: PRD Creation and Guidance

Capabilities:

  • Generate PRDs from template with all required sections
  • Ask clarifying questions to fill in gaps
  • Validate PRD completeness and quality
  • Reference existing PRDs for consistency
  • Ensure alignment with Morphir architecture
  • Include feature status tracking tables
  • Suggest implementation phases

Templates:

  • Standard feature PRD
  • Architecture change PRD
  • Breaking change PRD
  • Cross-repository PRD

Validation Checklist:

  • Problem statement clearly defined
  • Goals and non-goals explicit
  • User stories with acceptance criteria
  • Technical design outlined
  • Testing strategy included
  • Success criteria measurable
  • References to Morphir docs/architecture
  • Feature tracking table included

FR-2: Issue Creation and Enhancement

Capabilities:

  • Help craft feature requests
  • Help write bug reports
  • Help create enhancement proposals
  • Suggest appropriate labels and milestones
  • Cross-reference related issues
  • Validate issue completeness

Issue Templates:

  • Feature request
  • Bug report
  • Enhancement proposal
  • Documentation improvement
  • Performance issue

Quality Checklist:

  • Clear, descriptive title
  • Problem/motivation explained
  • Expected vs actual behavior (for bugs)
  • Steps to reproduce (for bugs)
  • Proposed solution or alternatives
  • Impact assessment
  • Links to related issues/docs
  • Appropriate labels

FR-3: Ecosystem Intelligence

Data Sources:

  • finos/morphir (core specs and schemas)
  • finos/morphir-elm (reference implementation)
  • finos/morphir-jvm (JVM implementation)
  • finos/morphir-scala (Scala implementation)
  • finos/morphir-dotnet (this repository)
  • finos/morphir-examples (examples and docs)

Intelligence Capabilities:

  • Query issues across all repositories
  • Track trending topics and labels
  • Identify common pain points
  • Monitor release cadences
  • Compare feature parity
  • Detect cross-repository dependencies

Metrics Tracked:

  • Issue velocity (opened, closed, avg time to close)
  • Backlog health (age distribution, staleness)
  • Label distribution and trends
  • Contributor activity
  • Documentation coverage
  • Test coverage trends

FR-4: GitHub Automation Scripts (F#)

Script: query-issues.fsx

// Query issues across Morphir repositories
// Usage: dotnet fsi query-issues.fsx --label "enhancement" --state "open" --repos "all"
// Output: JSON, Markdown, or formatted table

Features:

  • Multi-repository queries (all finos/morphir-* repos)
  • Filter by label, state, milestone, assignee, author
  • Sort by created, updated, comments, reactions
  • Format output as JSON, Markdown, or table
  • Cache results for performance

Script: analyze-backlog.fsx

// Analyze backlog health metrics
// Usage: dotnet fsi analyze-backlog.fsx --repo "finos/morphir-dotnet"
// Output: Health report with metrics and recommendations

Features:

  • Calculate backlog age distribution
  • Identify stale issues (no activity in 90+ days)
  • Analyze priority distribution
  • Compare against ecosystem averages
  • Generate recommendations for triage

Script: trend-analysis.fsx

// Identify trending topics across ecosystem
// Usage: dotnet fsi trend-analysis.fsx --since "30 days ago"
// Output: Trend report with top labels, themes, activity

Features:

  • Most active labels in time period
  • Emerging themes from issue titles/descriptions
  • Spike detection (unusual activity)
  • Cross-repository correlation
  • Sentiment analysis (positive/negative)

Script: check-ecosystem.fsx

// Check status across all Morphir repositories
// Usage: dotnet fsi check-ecosystem.fsx
// Output: Ecosystem health dashboard

Features:

  • Latest release versions
  • CI/CD status
  • Open PR counts
  • Recent activity summary
  • Documentation status
  • Test coverage (if available)

Script: generate-prd.fsx

// Generate PRD from template with interactive prompts
// Usage: dotnet fsi generate-prd.fsx --template "standard"
// Output: PRD markdown file

Features:

  • Interactive questionnaire for PRD sections
  • Pre-fill from existing issues or discussions
  • Validate completeness
  • Preview before saving
  • Save to docs/content/contributing/design/prds/

FR-5: Integration with Other Skills

With qa-tester:

  • Coordinate on acceptance criteria definition
  • Align test plans with PRD requirements
  • Validate feature completeness against PRD
  • Review test coverage for PRD features

With release-manager:

  • Align features with release roadmap
  • Coordinate changelog entries
  • Review “What’s New” documentation
  • Prioritize features for releases

With development agents:

  • Provide clear requirements and context
  • Answer questions during implementation
  • Validate implementation against PRD
  • Document design decisions in PRD

FR-6: Knowledge Management

Morphir Core Concepts:

  • Functional modeling approach
  • Type-driven development
  • Business domain modeling
  • Distribution and intermediate representation
  • Cross-language support strategy

Architecture Patterns:

  • Vertical Slice Architecture
  • Railway-oriented programming
  • ADT-first design
  • Immutability and pure functions
  • Effect management at boundaries

Decision-Making Framework:

  • IR fidelity over convenience
  • Minimize dependencies
  • Performance requires benchmarks
  • Keep effects at edges
  • Prefer explicit ADTs

Non-Functional Requirements

NFR-1: Response Time

  • Script execution < 30 seconds for single-repo queries
  • Script execution < 2 minutes for ecosystem-wide queries
  • PRD generation interactive (responds to each question in < 5 seconds)

NFR-2: Accuracy

  • Cross-repository queries return 100% accurate results
  • Trend analysis validated against manual review (>95% agreement)
  • Issue recommendations relevant (>80% user acceptance)

NFR-3: Maintainability

  • Scripts use GitHub CLI (gh) for authentication
  • Scripts use standard F# libraries (no exotic dependencies)
  • Scripts include help text and examples
  • Scripts handle rate limiting gracefully

NFR-4: Usability

  • Clear, conversational interaction style
  • Asks clarifying questions before making assumptions
  • Provides rationale for recommendations
  • Offers alternatives when pushing back
  • Links to relevant documentation

NFR-5: Documentation

  • Comprehensive skill.md with all capabilities
  • README with quick start guide
  • Script documentation with usage examples
  • Template documentation with instructions
  • Integration guide for other skills

Technical Design

Skill Structure

.claude/skills/product-manager/
├── skill.md                          # Main skill definition and playbooks
├── README.md                         # Quick start and overview
├── scripts/                          # F# automation scripts
│   ├── query-issues.fsx              # Multi-repo issue queries
│   ├── analyze-backlog.fsx           # Backlog health analysis
│   ├── trend-analysis.fsx            # Trend detection and reporting
│   ├── check-ecosystem.fsx           # Ecosystem status dashboard
│   ├── generate-prd.fsx              # Interactive PRD generation
│   ├── update-knowledge.fsx          # Update knowledgebase from live sources
│   └── common/                       # Shared utilities
│       ├── github-api.fsx            # GitHub API helpers
│       ├── formatting.fsx            # Output formatting
│       └── cache.fsx                 # Result caching
├── templates/                        # Document templates
│   ├── prd-standard.md               # Standard feature PRD
│   ├── prd-architecture.md           # Architecture change PRD
│   ├── prd-breaking.md               # Breaking change PRD
│   ├── issue-feature.md              # Feature request template
│   ├── issue-bug.md                  # Bug report template
│   └── issue-enhancement.md          # Enhancement proposal template
├── knowledge/                        # Curated knowledgebase (markdown)
│   ├── README.md                     # Knowledgebase overview and index
│   ├── morphir-principles.md         # Core Morphir philosophy and principles
│   ├── ecosystem-map.md              # Repository overview and relationships
│   ├── architecture/                 # Architecture patterns and decisions
│   │   ├── ir-design.md              # IR architecture and versioning
│   │   ├── vertical-slices.md        # Vertical Slice Architecture
│   │   ├── type-system.md            # Morphir type system
│   │   └── distribution-model.md     # Cross-language distribution
│   ├── repositories/                 # Per-repository knowledge
│   │   ├── morphir-core.md           # finos/morphir (specs)
│   │   ├── morphir-elm.md            # finos/morphir-elm (reference)
│   │   ├── morphir-jvm.md            # finos/morphir-jvm
│   │   ├── morphir-scala.md          # finos/morphir-scala
│   │   ├── morphir-dotnet.md         # finos/morphir-dotnet (this repo)
│   │   └── morphir-examples.md       # finos/morphir-examples
│   ├── features/                     # Feature status across repos
│   │   ├── cli-tools.md              # CLI feature parity
│   │   ├── ir-versions.md            # IR version support matrix
│   │   ├── backends.md               # Backend/codegen support
│   │   └── testing-tools.md          # Testing capabilities
│   ├── conventions/                  # Standards and conventions
│   │   ├── naming.md                 # Naming conventions
│   │   ├── code-style.md             # Code style per language
│   │   ├── commit-messages.md        # Commit message format
│   │   └── issue-labels.md           # Standard labels across repos
│   ├── workflows/                    # Common workflows and processes
│   │   ├── contributing.md           # Contribution workflow
│   │   ├── prd-process.md            # PRD creation and review
│   │   ├── release-process.md        # Release workflow
│   │   └── issue-triage.md           # Issue triage guidelines
│   └── faq/                          # Frequently asked questions
│       ├── product-decisions.md      # Common product decision rationales
│       ├── technical-choices.md      # Technical architecture FAQs
│       └── cross-repo-alignment.md   # How to align features across repos
└── docs/                             # Skill-specific documentation
    └── integration-guide.md          # Integration with other skills

Morphir Ecosystem Model

Repository Categories:

  1. Core Specification (finos/morphir)

    • Language specification
    • IR schema definitions (v1, v2, v3)
    • Authoritative documentation
  2. Reference Implementation (finos/morphir-elm)

    • Elm frontend compiler
    • CLI tools
    • Example models
    • Most mature implementation
  3. Platform Implementations:

    • finos/morphir-jvm: Java/Kotlin support
    • finos/morphir-scala: Scala support
    • finos/morphir-dotnet: C#/F# support
  4. Resources:

    • finos/morphir-examples: Example models and documentation

Cross-Repository Queries:

// Example: Find all IR-related issues across ecosystem
let irIssues =
    MorphirRepos.All
    |> Seq.collect (fun repo -> GitHub.queryIssues repo "label:IR")
    |> Seq.sortByDescending (_.UpdatedAt)

GitHub API Integration

Authentication:

  • Use GitHub CLI (gh) for authenticated requests
  • Leverage existing user credentials
  • No API tokens to manage

Rate Limiting:

  • Implement exponential backoff
  • Cache results for 15 minutes
  • Use GraphQL for complex queries (fewer requests)
  • Batch queries when possible

Query Patterns:

REST API (simple queries):

gh api repos/finos/morphir-dotnet/issues \
  --field state=open \
  --field labels=enhancement \
  --jq '.[] | {title, number, url}'

GraphQL API (complex queries):

query EcosystemIssues {
  search(query: "org:finos morphir in:name is:issue label:enhancement", type: ISSUE, first: 100) {
    nodes {
      ... on Issue {
        title
        number
        repository { name }
        labels(first: 10) { nodes { name } }
      }
    }
  }
}

Knowledgebase Management

Purpose: The Product Manager skill maintains a curated knowledgebase of Morphir ecosystem knowledge as markdown files within the skill directory. This enables offline access, version control, and structured knowledge organization.

Knowledge Categories:

  1. Core Principles (knowledge/morphir-principles.md)

    • Functional modeling philosophy
    • Type-driven development
    • Business domain modeling
    • Distribution strategy
    • Cross-language approach
  2. Ecosystem Map (knowledge/ecosystem-map.md)

    • Repository overview and purposes
    • Maturity levels and feature parity
    • Release cadences
    • Maintainer information
    • Dependency relationships
  3. Architecture (knowledge/architecture/)

    • IR design and versioning strategy
    • Vertical Slice Architecture patterns
    • Type system design
    • Distribution model
    • Backend architecture patterns
  4. Repository-Specific Knowledge (knowledge/repositories/)

    • Per-repo feature status
    • Technology stacks
    • Conventions and patterns
    • Common issues and solutions
    • Roadmap highlights
  5. Feature Parity (knowledge/features/)

    • CLI tools comparison matrix
    • IR version support across implementations
    • Backend/codegen capabilities
    • Testing tool availability
    • Documentation status
  6. Conventions (knowledge/conventions/)

    • Naming conventions (modules, types, functions)
    • Code style guides per language
    • Commit message standards
    • Issue/PR label taxonomy
    • Documentation standards
  7. Workflows (knowledge/workflows/)

    • Contribution process
    • PRD creation and review
    • Release management
    • Issue triage guidelines
    • Cross-repo coordination
  8. FAQs (knowledge/faq/)

    • Common product decision rationales
    • Technical architecture questions
    • Cross-repo alignment strategies
    • Migration and compatibility

Knowledge Update Workflow:

// update-knowledge.fsx: Fetch latest info from live sources
// Usage: dotnet fsi update-knowledge.fsx --category repositories

// Fetch latest README from each repo
let updateRepositoryDocs repos =
    repos
    |> Seq.iter (fun repo ->
        let readme = GitHub.fetchFile repo "README.md"
        let repoDoc = Knowledge.parseRepositoryInfo readme
        Knowledge.save $"knowledge/repositories/{repo.name}.md" repoDoc
    )

// Fetch latest feature status
let updateFeatureMatrix () =
    let cliFeatures =
        MorphirRepos.All
        |> Seq.collect (fun repo ->
            GitHub.searchCode repo "CLI commands"
        )
    Knowledge.generateFeatureMatrix cliFeatures
    |> Knowledge.save "knowledge/features/cli-tools.md"

// Validate knowledgebase consistency
let validateKnowledge () =
    Knowledge.checkBrokenLinks ()
    Knowledge.validateMarkdown ()
    Knowledge.checkOutdatedInfo ()

Knowledge Access Patterns:

When asked about Morphir principles:
1. Read knowledge/morphir-principles.md
2. Cite specific sections with links
3. Provide examples from knowledge/faq/

When comparing repos:
1. Read knowledge/ecosystem-map.md for overview
2. Read specific knowledge/repositories/{repo}.md
3. Consult knowledge/features/ for capability matrix

When validating feature alignment:
1. Reference knowledge/morphir-principles.md
2. Check knowledge/architecture/ for design patterns
3. Review knowledge/faq/product-decisions.md for precedents

Knowledge Maintenance:

  • Manual Curation: Maintainers update knowledge files as authoritative sources
  • Periodic Updates: Run update-knowledge.fsx quarterly to refresh from live sources
  • Version Control: Knowledge evolves with the skill, tracked in git
  • Validation: CI validates markdown formatting and internal links
  • Review Process: Knowledge changes reviewed like code changes

Knowledge vs. Live Data:

  • Knowledgebase: Stable, curated, architectural, and philosophical knowledge
  • Live Queries: Real-time issue data, PR status, recent activity
  • Hybrid Approach: Use knowledge for context, live queries for current state

PRD Template Engine

Interactive Generation:

// Prompt user for each section
let prd = PRD.Interactive [
    Section.Overview [
        Question "What feature are you proposing?"
        Question "Why is this feature needed?"
    ]
    Section.Goals [
        Question "What are the primary goals? (one per line)"
        Question "What is explicitly out of scope?"
    ]
    // ... more sections
]

// Validate completeness
let validation = PRD.validate prd

// Save to file
PRD.save "docs/content/contributing/design/prds/my-feature.md" prd

Skill Activation Triggers

Keywords:

  • “PRD”, “product requirements”, “feature spec”
  • “create issue”, “file bug”, “report enhancement”
  • “ecosystem”, “cross-repo”, “morphir repos”
  • “backlog”, “triage”, “issue health”
  • “trend”, “popular”, “common issues”
  • “align with morphir”, “morphir philosophy”

Scenarios:

  • User asks for help creating a PRD
  • User wants to file an issue
  • User asks “what should I work on?”
  • User asks about feature status across repos
  • User proposes a feature that may not align
  • User asks about Morphir architecture or principles

Feature Status Tracking

Feature IDFeatureStatusPriorityAssignedNotes
PM-01Skill definition (skill.md)⏳ PlannedP0-Core skill description and playbooks
PM-02README and quick start⏳ PlannedP0-User-facing documentation
PM-03Knowledgebase: morphir-principles.md⏳ PlannedP0-Core Morphir philosophy and principles
PM-04Knowledgebase: ecosystem-map.md⏳ PlannedP0-Repository overview and relationships
PM-05Knowledgebase: architecture/ (4 docs)⏳ PlannedP0-IR, VSA, type system, distribution
PM-06Knowledgebase: repositories/ (6 docs)⏳ PlannedP1-Per-repo knowledge
PM-07Knowledgebase: features/ (4 docs)⏳ PlannedP1-Feature parity matrices
PM-08Knowledgebase: conventions/ (4 docs)⏳ PlannedP1-Standards and conventions
PM-09Knowledgebase: workflows/ (4 docs)⏳ PlannedP1-Process documentation
PM-10Knowledgebase: faq/ (3 docs)⏳ PlannedP2-Frequently asked questions
PM-11PRD templates (standard, architecture, breaking)⏳ PlannedP0-Reusable PRD templates
PM-12Issue templates (feature, bug, enhancement)⏳ PlannedP0-Reusable issue templates
PM-13Script: query-issues.fsx⏳ PlannedP0-Multi-repo issue querying
PM-14Script: analyze-backlog.fsx⏳ PlannedP1-Backlog health metrics
PM-15Script: trend-analysis.fsx⏳ PlannedP1-Ecosystem trend detection
PM-16Script: check-ecosystem.fsx⏳ PlannedP1-Ecosystem status dashboard
PM-17Script: generate-prd.fsx⏳ PlannedP2-Interactive PRD generation
PM-18Script: update-knowledge.fsx⏳ PlannedP2-Update knowledgebase from live sources
PM-19Script utilities (GitHub API, formatting, cache)⏳ PlannedP0-Shared script infrastructure
PM-20Integration guide (with qa-tester, release-manager)⏳ PlannedP1-Cross-skill coordination
PM-21PRD creation playbook⏳ PlannedP0-Step-by-step PRD creation guide
PM-22Issue crafting playbook⏳ PlannedP0-Step-by-step issue creation guide
PM-23Ecosystem analysis playbook⏳ PlannedP1-How to analyze cross-repo trends
PM-24Feature validation playbook⏳ PlannedP1-Validate alignment with Morphir
PM-25Knowledge management playbook⏳ PlannedP2-How to maintain knowledgebase

Status Legend:

  • ⏳ Planned: Specification complete, ready to implement
  • 🚧 In Progress: Currently being implemented
  • ✅ Implemented: Feature complete and tested
  • 🔄 Iterating: Implemented but needs refinement
  • ⏸️ Deferred: Postponed to later phase

Priority Legend:

  • P0: Must-have for initial release
  • P1: Should-have for initial release
  • P2: Nice-to-have, can be added later

Implementation Phases

Phase 1: Core Infrastructure and Knowledgebase Foundation (Weeks 1-2)

Goal: Establish skill structure, foundational scripts, and core knowledgebase

Deliverables:

  • PRD created and reviewed (this document)
  • skill.md with core playbooks
  • README with quick start
  • Knowledgebase structure and README
  • knowledge/morphir-principles.md (P0)
  • knowledge/ecosystem-map.md (P0)
  • knowledge/architecture/ - 4 core docs (P0)
    • ir-design.md
    • vertical-slices.md
    • type-system.md
    • distribution-model.md
  • Basic GitHub API utilities (scripts/common/)
  • query-issues.fsx (basic functionality)

Success Criteria:

  • Skill can be invoked and responds appropriately
  • Knowledgebase has core Morphir principles documented
  • query-issues.fsx can query issues from single repository
  • Documentation explains skill purpose and capabilities
  • Skill can reference knowledgebase when answering questions

Phase 2: Templates, Playbooks, and Extended Knowledgebase (Weeks 2-3)

Goal: Provide templates, guided workflows, and expand knowledgebase

Deliverables:

  • All PRD templates (standard, architecture, breaking)
  • All issue templates (feature, bug, enhancement)
  • PRD creation playbook
  • Issue crafting playbook
  • Enhanced query-issues.fsx (multi-repo, filtering)
  • knowledge/repositories/ - 6 repo docs (P1)
  • knowledge/conventions/ - 4 convention docs (P1)
  • knowledge/workflows/ - 4 workflow docs (P1)

Success Criteria:

  • User can generate PRD using template
  • User can create well-structured issue with guidance
  • Multi-repository queries work across all finos/morphir-* repos
  • Knowledgebase covers all major Morphir repositories
  • Skill can compare features across repositories using knowledgebase

Phase 3: Analytics, Intelligence, and Feature Matrices (Weeks 3-4)

Goal: Add ecosystem intelligence capabilities and feature comparison matrices

Deliverables:

  • analyze-backlog.fsx
  • trend-analysis.fsx
  • check-ecosystem.fsx
  • Feature validation playbook
  • Ecosystem analysis playbook
  • Caching infrastructure
  • knowledge/features/ - 4 feature matrices (P1)
  • knowledge/faq/ - 3 FAQ docs (P2)

Success Criteria:

  • Backlog health metrics accurate and actionable
  • Trend analysis identifies real patterns (validated manually)
  • Ecosystem dashboard provides useful overview
  • Feature matrices enable cross-repo capability comparisons
  • FAQs capture common product decision rationales

Phase 4: Integration, Polish, and Knowledge Automation (Week 4-5)

Goal: Integrate with other skills, refine, and add knowledge automation

Deliverables:

  • Integration guide (qa-tester, release-manager)
  • generate-prd.fsx (interactive PRD generation)
  • update-knowledge.fsx (knowledgebase automation)
  • Knowledge management playbook
  • Comprehensive testing
  • Documentation review and updates
  • Example walkthroughs
  • Knowledgebase validation (CI integration)

Success Criteria:

  • Skill integrates smoothly with qa-tester and release-manager
  • generate-prd.fsx creates complete, high-quality PRDs
  • update-knowledge.fsx can refresh knowledgebase from live sources
  • Documentation is comprehensive and clear
  • Examples demonstrate all major workflows
  • Knowledgebase passes automated validation checks

Testing Strategy

Manual Testing

PRD Creation:

  1. Request PRD for fictional feature
  2. Validate all sections populated
  3. Check alignment with existing PRDs
  4. Verify feature tracking table included

Issue Creation:

  1. Request help creating feature, bug, enhancement
  2. Validate templates used correctly
  3. Check cross-references to related issues
  4. Verify appropriate labels suggested

Ecosystem Queries:

  1. Run query-issues.fsx across all repos
  2. Validate results accuracy (spot check 20 issues)
  3. Test filtering, sorting, formatting
  4. Verify performance < 2 minutes

Backlog Analysis:

  1. Run analyze-backlog.fsx on known repo
  2. Manually validate metrics (age, staleness)
  3. Check recommendations are actionable
  4. Compare against ecosystem averages

Trend Analysis:

  1. Run trend-analysis.fsx for 30-day window
  2. Manually review top trending labels
  3. Validate emerging themes make sense
  4. Check for false positives

Integration Testing

With qa-tester:

  1. Create PRD, then ask qa-tester for test plan
  2. Verify test plan aligns with PRD acceptance criteria
  3. Check cross-references work

With release-manager:

  1. Ask about feature priority for release
  2. Verify release-manager can access PRD context
  3. Check coordination on changelog entries

Acceptance Testing

User Scenarios:

  • New contributor creates first issue with PM help
  • Maintainer generates PRD for complex feature
  • Developer checks ecosystem for related work
  • Project lead analyzes backlog health
  • Contributor validates feature alignment

Quality Checks:

  • PRDs follow template structure
  • Issues have appropriate labels
  • Cross-repo queries are accurate
  • Metrics are validated against manual checks
  • Recommendations are helpful (user survey)

Success Criteria

Quantitative Metrics

  • Adoption: 80% of PRDs created using Product Manager skill
  • Issue Quality: 90% of issues created with PM help are well-structured (manual review)
  • Query Accuracy: 95% precision on cross-repo issue searches
  • Performance: All scripts complete within SLA (30s single-repo, 2min ecosystem)
  • Coverage: All 6 Morphir repos covered by ecosystem queries

Qualitative Metrics

  • User Satisfaction: Positive feedback from 4+ contributors
  • Maintainer Impact: Reduced time spent triaging issues
  • Knowledge Transfer: New contributors feel confident creating issues/PRDs
  • Alignment: Features better aligned with Morphir philosophy (maintainer assessment)
  • Integration: Smooth coordination with qa-tester and release-manager

Completion Criteria

  • All P0 features implemented and tested
  • All P1 features implemented and tested
  • Documentation complete and reviewed
  • Integration tested with other skills
  • At least 3 real PRDs created using the skill
  • At least 10 real issues created with PM assistance
  • Ecosystem queries validated across all repos
  • Maintainer sign-off

Implementation Notes

2025-12-18: Initial PRD Creation

  • Decision: Start with comprehensive PRD before implementation
  • Rationale: Complex skill requiring careful design and alignment with existing patterns
  • Impact: Clear roadmap for phased implementation
  • Files: This PRD (product-manager-skill.md)

Open Questions

Q1: Should the Product Manager skill fetch live documentation from morphir.finos.org?

Status: Open Options:

  1. Fetch live docs via WebFetch tool
  2. Maintain local cache of key documentation
  3. Reference docs via links only

Decision Needed By: Phase 1 (Week 1) Impact: Affects skill.md design and response accuracy

Q2: How should the skill handle conflicting guidance across repos?

Status: Open Example: morphir-elm uses one convention, morphir-dotnet uses another Options:

  1. Always favor reference implementation (morphir-elm)
  2. Favor current repo context
  3. Present both and explain tradeoffs

Decision Needed By: Phase 1 (Week 1) Impact: Affects ecosystem map and playbook design

Q3: Should F# scripts use GitHub CLI or direct API calls?

Status: Open Options:

  1. GitHub CLI (gh) for simplicity and auth
  2. Direct API calls via HTTP client for flexibility
  3. Hybrid approach

Recommendation: GitHub CLI for Phase 1, evaluate direct API if needed Decision Needed By: Phase 1 (Week 1) Impact: Affects script architecture and dependencies

Q4: How deep should trend analysis go?

Status: Open Options:

  1. Label frequency and time-series only
  2. Add NLP for theme extraction from titles/descriptions
  3. Add sentiment analysis

Recommendation: Start with label frequency, add NLP in Phase 3 if valuable Decision Needed By: Phase 3 (Week 3) Impact: Affects trend-analysis.fsx complexity and dependencies

References

Project Documentation

Morphir Resources


Last Updated: 2025-12-18 Next Review: After Phase 1 completion (Week 2)

16.1.6.4 - PRD: Deployment Architecture Refactor

Refactor deployment architecture to fix packaging issues and establish changelog-driven versioning

PRD: Morphir .NET Deployment Architecture Refactor

Executive Summary

Refactor the morphir-dotnet deployment architecture to fix critical packaging issues, separate tool distribution from executable distribution, implement comprehensive build testing, and establish changelog-driven versioning as the single source of truth.

Problem: The current deployment failed due to package naming mismatches (lowercase “morphir” vs “Morphir”), inconsistent tool command naming, and lack of automated testing to catch these issues before CI deployment.

Solution: Separate concerns into distinct projects (Morphir.Tool for dotnet tool, Morphir for executables), reorganize build system following vertical slice architecture, implement Ionide.KeepAChangelog for version management, and add comprehensive build testing infrastructure.

Impact: Eliminates deployment failures, provides clear distribution strategy for different user personas, enables confident releases with automated validation, and establishes maintainable build architecture.


Table of Contents

  1. Background
  2. Problem Statement
  3. Goals & Non-Goals
  4. User Personas
  5. Design Decisions
  6. Architecture
  7. Implementation Plan
  8. BDD Acceptance Criteria
  9. Testing Strategy
  10. Risks & Mitigation
  11. Success Metrics
  12. Timeline
  13. References

Background

Current State

The morphir-dotnet project currently:

  • Uses a single Morphir project for both tool and executable
  • Has AssemblyName “morphir” (lowercase) causing glob pattern mismatches
  • Sets version via RELEASE_VERSION environment variable
  • Has no automated tests for packaging or deployment
  • Suffers from configuration inconsistency (tool command as “morphir” vs “dotnet-morphir”)

Recent Failure

Deployment to main (run #20330271677) failed with:

System.Exception: Morphir tool package not found in /artifacts/packages
  at Build.<get_PublishTool>b__71_1() in Build.cs:line 462

Root cause: Build.cs searches for Morphir.*.nupkg (capital M) but package is named morphir.*.nupkg (lowercase m) due to AssemblyName mismatch.

Research Conducted

Analyzed industry patterns including:

  • Nuke build system’s own packaging strategy
  • Other .NET CLI tools (dotnet-format, dotnet-ef, GitVersion)
  • Ionide.KeepAChangelog for changelog-driven versioning
  • TestContainers for local NuGet server testing
  • Keep a Changelog specification for pre-release versioning

Problem Statement

Critical Issues

  1. Package Naming Mismatch ⚠️ BLOCKER

    • Build.cs expects Morphir.*.nupkg
    • Actual package: morphir.*.nupkg
    • Deployment fails at PublishTool step
  2. Tool Command Inconsistency

    • Build.cs: ToolCommandName=morphir
    • Deprecated scripts: ToolCommandName=dotnet-morphir
    • Install scripts reference inconsistent command names
  3. No Build Testing

    • No validation of package structure
    • No test of tool installation
    • Issues only discovered in CI deployment
    • Manual verification required
  4. Architectural Confusion

    • Single project serves both tool and executable
    • Mixed concerns (dotnet tool + AOT compilation)
    • Difficult to optimize for each use case
    • Complex build configuration
  5. Version Management Fragility

    • Manual RELEASE_VERSION in workflow file
    • No validation or enforcement
    • Risk of version drift between packages
    • CHANGELOG.md not connected to versions

User Impact

Before fix:

  • Users confused about tool command name
  • Documentation doesn’t match reality
  • Deployment failures block releases
  • Manual verification slows development

After fix:

  • Clear persona-based installation paths
  • Automated validation prevents failures
  • Confident, fast releases
  • Maintainable architecture

Goals & Non-Goals

Goals

✅ Fix immediate deployment failure

  • Resolve package naming mismatch
  • Successful deployment to NuGet.org and GitHub Releases

✅ Separate concerns

  • Distinct Morphir.Tool project for dotnet tool
  • Morphir project for standalone executables
  • Clear boundaries and responsibilities

✅ Implement comprehensive testing

  • Package structure validation
  • Metadata correctness verification
  • Local installation smoke tests
  • Catch issues before CI deployment

✅ Establish changelog-driven versioning

  • CHANGELOG.md as single source of truth
  • Ionide.KeepAChangelog integration
  • Support pre-release versions (alpha, beta, rc)
  • Automated release preparation

✅ Dual distribution strategy

  • NuGet tool package for .NET developers
  • GitHub releases with executables for non-SDK users
  • Persona-based documentation

✅ Organize build system

  • Split Build.cs by domain (vertical slices)
  • Extract helper classes for testability
  • Align with Morphir.Tooling architecture
  • Maintainable and scalable structure

Non-Goals

❌ Automated pre-release version bumping (Phase 2, future work) ❌ TestContainers integration (Phase 3 of testing, when needed) ❌ Package rename/migration (Keeping current names for backward compatibility) ❌ Breaking changes to public APIs (Maintain compatibility)


User Personas

Persona 1: .NET Developer

Profile:

  • Has .NET SDK installed (development machine)
  • Uses dotnet CLI regularly
  • Works with Morphir in .NET projects
  • Expects standard dotnet tooling experience

Needs:

  • dotnet tool install -g Morphir.Tool
  • Automatic updates via dotnet tool update
  • Integration with IDEs and build tools
  • Familiar dotnet conventions

Distribution: NuGet.org package

Command: morphir (after tool install)


Persona 2: Shell Script / Container User

Profile:

  • Minimal environment (Alpine Linux, slim containers)
  • Cannot install .NET SDK (size constraints)
  • Uses Morphir as CLI utility in scripts
  • Needs fast startup, small binary

Needs:

  • Standalone executable (no SDK required)
  • AOT-compiled for fast startup
  • Small binary size (trimmed)
  • Install via curl/wget script

Distribution: GitHub Releases with platform-specific executables

Command: morphir (or ./morphir-linux-x64)


Persona 3: CI/CD Pipeline

Profile:

  • GitHub Actions, GitLab CI, Jenkins
  • May or may not have .NET SDK pre-installed
  • Speed and caching are priorities
  • Reliability is critical

Needs:

  • Flexible installation (either method works)
  • Fast downloads and caching
  • Consistent behavior across runs
  • Clear error messages

Distribution: Either NuGet tool or GitHub release executable

Command: morphir


Design Decisions

All design decisions were made interactively with stakeholders. See Design Decision Rationale for full context.

Decision 1: Project Structure

Decision: Separate projects (Option B)

Create new src/Morphir.Tool/ project for dotnet tool, keep src/Morphir/ for standalone executable.

Rationale:

  • Clear separation of concerns
  • Industry pattern (matches Nuke, GitVersion)
  • Easier to test independently
  • Optimized for each use case

Alternatives considered:

  • Keep single project with renamed package (doesn’t fix architecture)
  • Keep current, fix naming only (addresses symptom, not cause)

Decision 2: Testing Strategy

Decision: Hybrid approach (Option C)

  • Phase 1: Package structure + metadata validation (no Docker)
  • Phase 2: Local folder smoke tests
  • Phase 3: TestContainers + BaGet (future, when needed)

Rationale:

  • Pragmatic - start simple, add complexity when needed
  • Fast feedback loop (no Docker startup)
  • Covers 80% of issues immediately
  • Extensible for future enhancements

Alternatives considered:

  • Folder-based only (insufficient validation)
  • Full TestContainers immediately (overkill, slower)

Decision 3: Build Organization

Decision: Split by domain + extract helpers (Option B + D hybrid)

Split Build.cs into:

  • Build.cs - Entry point, core configuration
  • Build.Packaging.cs - Pack targets
  • Build.Publishing.cs - Publish targets
  • Build.Testing.cs - Test targets
  • Helpers/ - PackageValidator, ChangelogHelper, etc.

Rationale:

  • Aligns with vertical slice architecture (matches Morphir.Tooling)
  • Clear feature boundaries
  • Testable helper classes
  • Scales well as features grow

Alternatives considered:

  • Keep single file (will become unwieldy)
  • Split by technical concern (doesn’t match domain boundaries)

Decision 4: Distribution Strategy

Decision: Dual distribution (Option B)

  • NuGet.org: Morphir.Tool package (dotnet tool)
  • GitHub Releases: Platform executables (linux-x64, win-x64, osx-arm64, etc.)

Rationale:

  • Serves all user personas
  • Industry standard pattern
  • Optimal for each use case
  • Flexible deployment options

Alternatives considered:

  • Tool package only (excludes non-SDK users)
  • Executable only (not idiomatic for .NET developers)

Decision 5: Version Management

Decision: CHANGELOG.md as single source of truth via Ionide.KeepAChangelog (Option D)

  • Use Ionide.KeepAChangelog to extract version from CHANGELOG.md
  • Support pre-release versions (alpha, beta, rc, preview)
  • PrepareRelease target automates [Unreleased] → [X.Y.Z] promotion
  • Auto-bump pre-release based on prior pre-release type
  • Git tags use v prefix (e.g., v0.2.1)

Rationale:

  • Respects Keep a Changelog workflow
  • Enforces changelog updates before release
  • Supports pre-release versioning fully
  • Single source of truth (no version.json needed)
  • Automates tedious changelog formatting

Alternatives considered:

  • Environment variable only (error-prone, no validation)
  • version.json (duplication with CHANGELOG.md)
  • GitVersion only (doesn’t fit changelog-driven workflow)

Architecture

Project Structure

morphir-dotnet/
├── src/
│   ├── Morphir/                      # Standalone executable (AOT)
│   │   ├── Morphir.csproj
│   │   ├── Program.cs
│   │   └── (Output: morphir-{rid} executables)
│   │
│   ├── Morphir.Tool/                 # NEW - Dotnet tool (managed DLLs)
│   │   ├── Morphir.Tool.csproj
│   │   ├── Program.cs                # Thin wrapper
│   │   └── (Output: Morphir.Tool.nupkg)
│   │
│   ├── Morphir.Core/                 # Core domain
│   └── Morphir.Tooling/              # Tooling services
│
├── tests/
│   ├── Morphir.Build.Tests/          # NEW - Build system tests
│   │   ├── PackageStructureTests.cs
│   │   ├── PackageMetadataTests.cs
│   │   └── LocalInstallationTests.cs
│   ├── Morphir.Core.Tests/
│   ├── Morphir.Tooling.Tests/
│   └── Morphir.BlackBox.Tests/
│
├── build/
│   ├── Build.cs                      # Entry point
│   ├── Build.Packaging.cs            # NEW - Pack targets
│   ├── Build.Publishing.cs           # NEW - Publish targets
│   ├── Build.Testing.cs              # NEW - Test targets
│   └── Helpers/                      # NEW
│       ├── PackageValidator.cs
│       ├── ChangelogHelper.cs
│       └── PathHelper.cs
│
├── CHANGELOG.md                      # Single source of truth for versions
└── .github/workflows/
    └── deployment.yml                # Updated for new architecture

Package Relationships

Morphir.Tool (NuGet package)
  └── depends on Morphir.Core
  └── depends on Morphir.Tooling

Morphir.Core (NuGet package)
  └── standalone library

Morphir.Tooling (NuGet package)
  └── depends on Morphir.Core

Morphir executables (GitHub releases)
  └── self-contained, no dependencies
  └── AOT-compiled, trimmed

Build Flow

┌─────────────────────────────────────────────────────────────┐
│ Developer: Update CHANGELOG.md [Unreleased]                 │
│            Add changes to feature branch                     │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│ Release Prep: ./build.sh PrepareRelease --version 0.2.1     │
│               Moves [Unreleased] → [0.2.1] - YYYY-MM-DD     │
│               Creates release/0.2.1 branch                   │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│ PR to main: Code review, approval, merge                    │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│ Tag push: git tag -a v0.2.1 -m "Release 0.2.1"             │
│           git push origin v0.2.1                             │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│ CI Deployment:                                               │
│ 1. Extract version from CHANGELOG.md (0.2.1)                │
│ 2. Run build tests (validate packages)                      │
│ 3. Build Morphir.Tool.nupkg → NuGet.org                     │
│ 4. Build executables → GitHub Release v0.2.1                │
│ 5. Upload executables to release                            │
│ 6. Extract release notes from CHANGELOG.md                  │
└──────────────────────────────────────────────────────────────┘

Versioning Flow

CHANGELOG.md [Unreleased]
  └── Developer adds changes here during development

PrepareRelease --version 0.2.1
  └── [Unreleased] → [0.2.1] - 2025-12-20
  └── Update comparison links
  └── Stage changes (manual commit)

Ionide.KeepAChangelog
  └── Parses CHANGELOG.md
  └── Extracts latest version: 0.2.1
  └── Extracts release notes for PackageReleaseNotes

Nuke Build
  └── GetVersionFromChangelog() → SemVersion 0.2.1
  └── All Pack targets use this version
  └── All packages have same version

Pre-release Versioning

CHANGELOG.md:
## [0.2.1-alpha.1] - 2025-12-18
## [0.2.1-alpha.2] - 2025-12-19  ← Auto-bumped
## [0.2.1-beta.1] - 2025-12-20   ← Explicit release
## [0.2.1-beta.2] - 2025-12-21   ← Auto-bumped
## [0.2.1-rc.1] - 2025-12-22     ← Explicit release
## [0.2.1] - 2025-12-23          ← Final release

Auto-bump logic:
- Detect previous pre-release type (alpha/beta/preview/rc)
- Increment number: alpha.1 → alpha.2
- On new explicit type: beta.1 (resets number)

Implementation Plan

Phase 1: Project Structure & Build Organization (3-4 days)

Goal: Separate projects, reorganize build system

Tasks

1.1 Create Morphir.Tool Project

  • Create src/Morphir.Tool/ directory
  • Create Morphir.Tool.csproj:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <OutputType>Exe</OutputType>
        <PackAsTool>true</PackAsTool>
        <ToolCommandName>morphir</ToolCommandName>
        <PackageId>Morphir.Tool</PackageId>
        <IsPackable>true</IsPackable>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="../Morphir.Core/Morphir.Core.csproj" />
        <ProjectReference Include="../Morphir.Tooling/Morphir.Tooling.csproj" />
      </ItemGroup>
    </Project>
    
  • Create minimal Program.cs:
    // Delegates to Morphir.Tooling
    return await Morphir.Tooling.CLI.RunAsync(args);
    
  • Add to solution file

1.2 Update Morphir Project

  • Ensure Morphir.csproj has AssemblyName="morphir" (lowercase)
  • Verify IsPackable=false (not published to NuGet)
  • Ensure AOT and trimming settings remain
  • Keep current Program.cs unchanged

1.3 Split Build.cs

  • Create build/Build.Packaging.cs:
    partial class Build
    {
        Target PackLibs => _ => _...
        Target PackTool => _ => _...
        Target PackAll => _ => _...
    }
    
  • Create build/Build.Publishing.cs:
    partial class Build
    {
        Target PublishLibs => _ => _...
        Target PublishTool => _ => _...
        Target PublishAll => _ => _...
        Target PublishLocalLibs => _ => _...
        Target PublishLocalTool => _ => _...
    }
    
  • Create build/Build.Testing.cs:
    partial class Build
    {
        Target Test => _ => _...
        Target TestE2E => _ => _...
        Target TestBuild => _ => _... // NEW
        Target TestAll => _ => _...
    }
    
  • Keep Build.cs as main entry with:
    • Parameters
    • Core configuration
    • Main targets (Restore, Compile, Clean)
    • CI orchestration targets

1.4 Create Helper Classes

  • Create build/Helpers/ directory
  • Create PackageValidator.cs:
    public static class PackageValidator
    {
        public static void ValidateToolPackage(AbsolutePath packagePath) { }
        public static void ValidateLibraryPackage(AbsolutePath packagePath) { }
    }
    
  • Create ChangelogHelper.cs:
    public static class ChangelogHelper
    {
        public static SemVersion GetVersionFromChangelog(AbsolutePath changelogPath) { }
        public static string GetReleaseNotes(AbsolutePath changelogPath) { }
        public static void PrepareRelease(AbsolutePath changelogPath, string version) { }
    }
    
  • Create PathHelper.cs:
    public static class PathHelper
    {
        public static AbsolutePath FindLatestPackage(AbsolutePath directory, string pattern) { }
    }
    

1.5 Remove Deprecated Code

  • Delete scripts/pack-tool-platform.cs
  • Delete scripts/build-tool-dll.cs
  • Remove references from documentation
  • Update NUKE_MIGRATION.md

1.6 Update Build Targets

  • Fix PackTool to build Morphir.Tool.csproj:
    Target PackTool => _ => _
        .DependsOn(Compile)
        .Executes(() => {
            DotNetPack(s => s
                .SetProject(RootDirectory / "src" / "Morphir.Tool" / "Morphir.Tool.csproj")
                .SetConfiguration(Configuration)
                .SetVersion(Version.ToString())
                .SetOutputDirectory(OutputDir));
        });
    
  • Fix PublishTool glob pattern:
    var toolPackage = OutputDir.GlobFiles("Morphir.Tool.*.nupkg")
        .FirstOrDefault();
    

BDD Tests:

Feature: Project structure refactor
  Scenario: Build Morphir.Tool package
    Given Morphir.Tool project exists
    When I run "./build.sh PackTool"
    Then Morphir.Tool.*.nupkg should be created
    And package should contain tools/net10.0/any/morphir.dll

  Scenario: Build split successfully
    Given Build.cs is split into partial classes
    When I run "./build.sh --help"
    Then all targets should be available
    And no build errors should occur

Phase 2: Changelog-Driven Versioning (2-3 days)

Goal: Integrate Ionide.KeepAChangelog, implement PrepareRelease

Tasks

2.1 Add Ionide.KeepAChangelog

  • Add package to build/_build.csproj:
    cd build
    dotnet add package Ionide.KeepAChangelog --version 0.2.0
    
  • Add using statement to Build.cs:
    using KeepAChangelogParser;
    using Semver;
    

2.2 Implement Version Extraction

  • Create ChangelogHelper.GetVersionFromChangelog():
    public static SemVersion GetVersionFromChangelog(AbsolutePath changelogPath)
    {
        var content = File.ReadAllText(changelogPath);
        var parser = new ChangelogParser();
        var result = parser.Parse(content);
    
        if (!result.IsSuccess)
            throw new Exception($"Failed to parse CHANGELOG.md: {result.Error}");
    
        var changelog = result.Value;
        var latest = changelog.SectionCollection.FirstOrDefault()
            ?? throw new Exception("No releases found in CHANGELOG.md");
    
        if (!SemVersion.TryParse(latest.MarkdownVersion, SemVersionStyles.Any, out var version))
            throw new Exception($"Invalid version: {latest.MarkdownVersion}");
    
        return version;
    }
    

2.3 Implement Release Notes Extraction

  • Create ChangelogHelper.GetReleaseNotes():
    public static string GetReleaseNotes(AbsolutePath changelogPath)
    {
        var content = File.ReadAllText(changelogPath);
        var parser = new ChangelogParser();
        var result = parser.Parse(content);
    
        if (!result.IsSuccess) return string.Empty;
    
        var latest = result.Value.SectionCollection.FirstOrDefault();
        if (latest == null) return string.Empty;
    
        var notes = new StringBuilder();
        AppendSection("Added", latest.SubSections.Added);
        AppendSection("Changed", latest.SubSections.Changed);
        // ... other sections
        return notes.ToString();
    }
    

2.4 Update Build.cs to Use Changelog

  • Add property:
    SemVersion Version => ChangelogHelper.GetVersionFromChangelog(ChangelogFile);
    string ReleaseNotes => ChangelogHelper.GetReleaseNotes(ChangelogFile);
    AbsolutePath ChangelogFile => RootDirectory / "CHANGELOG.md";
    
  • Update all Pack targets to use Version property
  • Update all Pack targets to use ReleaseNotes for PackageReleaseNotes

2.5 Implement PrepareRelease Target

  • Create target in Build.Publishing.cs:
    [Parameter("Version to release")] readonly string ReleaseVersion;
    
    Target PrepareRelease => _ => _
        .Description("Prepare a new release: moves [Unreleased] to [X.Y.Z]")
        .Requires(() => ReleaseVersion)
        .Executes(() =>
        {
            // 1. Validate version
            if (!SemVersion.TryParse(ReleaseVersion, out var version))
                throw new Exception($"Invalid version: {ReleaseVersion}");
    
            // 2. Validate [Unreleased] has content
            if (!ChangelogHelper.HasUnreleasedContent(ChangelogFile))
                throw new Exception("[Unreleased] section is empty");
    
            // 3. Update CHANGELOG.md
            ChangelogHelper.PrepareRelease(ChangelogFile, ReleaseVersion);
    
            // 4. Stage changes
            Git("add CHANGELOG.md");
    
            // 5. Show next steps
            Serilog.Log.Information("✓ Prepared release {0}", ReleaseVersion);
            Serilog.Log.Information("Next steps:");
            Serilog.Log.Information("  1. Review: git diff --staged");
            Serilog.Log.Information("  2. Commit: git commit -m 'chore: prepare release {0}'", ReleaseVersion);
            Serilog.Log.Information("  3. Push: git push origin release/{0}", ReleaseVersion);
            Serilog.Log.Information("  4. Create PR to main");
            Serilog.Log.Information("  5. After merge, tag: git tag -a v{0} -m 'Release {0}'", ReleaseVersion);
            Serilog.Log.Information("  6. Push tag: git push origin v{0}", ReleaseVersion);
        });
    

2.6 Implement Changelog Manipulation

  • Create ChangelogHelper.HasUnreleasedContent():
    public static bool HasUnreleasedContent(AbsolutePath changelogPath)
    {
        var content = File.ReadAllText(changelogPath);
        var unreleasedPattern = @"\[Unreleased\][\s\S]*?(?=\[[\d\.]|\z)";
        var match = Regex.Match(content, unreleasedPattern);
        return match.Success && (match.Value.Contains("- ") || match.Value.Contains("* "));
    }
    
  • Create ChangelogHelper.PrepareRelease():
    public static void PrepareRelease(AbsolutePath changelogPath, string version)
    {
        var content = File.ReadAllText(changelogPath);
        var date = DateTime.Now.ToString("yyyy-MM-dd");
    
        // Extract [Unreleased] content
        var unreleasedPattern = @"## \[Unreleased\](.*?)(?=## \[|$)";
        var match = Regex.Match(content, unreleasedPattern, RegexOptions.Singleline);
    
        if (!match.Success)
            throw new Exception("Could not find [Unreleased] section");
    
        var unreleasedContent = match.Groups[1].Value.Trim();
    
        // Create new sections
        var newUnreleased = "## [Unreleased]\n\n";
        var newRelease = $"## [{version}] - {date}\n\n{unreleasedContent}\n\n";
    
        // Replace [Unreleased] with both sections
        var updated = Regex.Replace(
            content,
            unreleasedPattern,
            newUnreleased + newRelease,
            RegexOptions.Singleline
        );
    
        // Update comparison links
        updated = UpdateComparisonLinks(updated, version);
    
        File.WriteAllText(changelogPath, updated);
    }
    

2.7 Implement Auto Pre-release Bumping

  • Create ChangelogHelper.GetNextPreReleaseVersion():
    public static SemVersion GetNextPreReleaseVersion(AbsolutePath changelogPath)
    {
        var currentVersion = GetVersionFromChangelog(changelogPath);
    
        if (!currentVersion.IsPrerelease)
            throw new Exception("Cannot auto-bump non-prerelease version");
    
        // Extract pre-release type and number
        // e.g., "alpha.1" → type: "alpha", number: 1
        var prereleaseParts = currentVersion.Prerelease.Split('.');
        var type = prereleaseParts[0]; // alpha, beta, preview, rc
        var number = int.Parse(prereleaseParts.Length > 1 ? prereleaseParts[1] : "0");
    
        // Increment number
        number++;
    
        // Create new version
        var newPrerelease = $"{type}.{number}";
        return new SemVersion(
            currentVersion.Major,
            currentVersion.Minor,
            currentVersion.Patch,
            newPrerelease
        );
    }
    
  • Create target for auto-bump (used in CI):
    Target BumpPreRelease => _ => _
        .Description("Auto-bump pre-release version (CI only)")
        .Executes(() =>
        {
            var currentVersion = Version;
    
            if (!currentVersion.IsPrerelease)
            {
                Serilog.Log.Information("Not a pre-release, skipping auto-bump");
                return;
            }
    
            var nextVersion = ChangelogHelper.GetNextPreReleaseVersion(ChangelogFile);
            Serilog.Log.Information("Auto-bumping {0} → {1}", currentVersion, nextVersion);
    
            // Update CHANGELOG.md with empty section for next pre-release
            ChangelogHelper.AddPreReleaseSection(ChangelogFile, nextVersion.ToString());
        });
    

BDD Tests:

Feature: Changelog-driven versioning
  Scenario: Extract version from CHANGELOG
    Given CHANGELOG.md has [0.2.1] - 2025-12-20
    When I call GetVersionFromChangelog()
    Then version should be 0.2.1

  Scenario: Prepare release
    Given CHANGELOG.md has [Unreleased] with content
    When I run "./build.sh PrepareRelease --version 0.2.1"
    Then CHANGELOG.md should have [0.2.1] - 2025-12-20
    And [Unreleased] should be empty
    And changes should be staged

  Scenario: Block release without content
    Given CHANGELOG.md [Unreleased] is empty
    When I run "./build.sh PrepareRelease --version 0.2.1"
    Then build should fail
    And error should mention "empty"

Phase 3: Build Testing Infrastructure (3-4 days)

Goal: Create comprehensive build tests

Tasks

3.1 Create Test Project

  • Create tests/Morphir.Build.Tests/ directory
  • Create Morphir.Build.Tests.csproj:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <IsPackable>false</IsPackable>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="TUnit" />
        <PackageReference Include="FluentAssertions" />
        <PackageReference Include="System.IO.Compression" />
      </ItemGroup>
    </Project>
    
  • Create test infrastructure:
    public class TestFixture
    {
        public AbsolutePath ArtifactsDir { get; }
        public AbsolutePath FindPackage(string pattern) { }
    }
    

3.2 Package Structure Tests

  • Create PackageStructureTests.cs:
    [Test]
    public async Task ToolPackage_HasCorrectStructure()
    {
        // Arrange
        var package = FindLatestPackage("Morphir.Tool.*.nupkg");
    
        // Act
        using var archive = ZipFile.OpenRead(package);
        var entries = archive.Entries.Select(e => e.FullName).ToList();
    
        // Assert
        entries.Should().Contain("tools/net10.0/any/morphir.dll");
        entries.Should().Contain("tools/net10.0/any/DotnetToolSettings.xml");
        entries.Should().Contain("tools/net10.0/any/Morphir.Core.dll");
        entries.Should().Contain("tools/net10.0/any/Morphir.Tooling.dll");
    }
    
    [Test]
    public async Task ToolPackage_HasCorrectToolSettings()
    {
        var package = FindLatestPackage("Morphir.Tool.*.nupkg");
    
        using var archive = ZipFile.OpenRead(package);
        var entry = archive.GetEntry("tools/net10.0/any/DotnetToolSettings.xml");
    
        using var reader = new StreamReader(entry.Open());
        var xml = await reader.ReadToEndAsync();
    
        xml.Should().Contain("<Command Name=\"morphir\"");
        xml.Should().Contain("EntryPoint=\"morphir.dll\"");
    }
    
    [Test]
    public async Task LibraryPackages_HaveCorrectStructure()
    {
        var corePackage = FindLatestPackage("Morphir.Core.*.nupkg");
    
        using var archive = ZipFile.OpenRead(corePackage);
        var entries = archive.Entries.Select(e => e.FullName).ToList();
    
        entries.Should().Contain(e => e.Contains("lib/net10.0/Morphir.Core.dll"));
        entries.Should().NotContain(e => e.Contains("tools/"));
    }
    

3.3 Package Metadata Tests

  • Create PackageMetadataTests.cs:
    [Test]
    public async Task AllPackages_HaveSameVersion()
    {
        var corePackage = FindLatestPackage("Morphir.Core.*.nupkg");
        var toolingPackage = FindLatestPackage("Morphir.Tooling.*.nupkg");
        var toolPackage = FindLatestPackage("Morphir.Tool.*.nupkg");
    
        var coreVersion = GetPackageVersion(corePackage);
        var toolingVersion = GetPackageVersion(toolingPackage);
        var toolVersion = GetPackageVersion(toolPackage);
    
        coreVersion.Should().Be(toolingVersion);
        coreVersion.Should().Be(toolVersion);
    }
    
    [Test]
    public async Task AllPackages_HaveVersionFromChangelog()
    {
        var changelogVersion = GetVersionFromChangelog();
        var toolPackage = FindLatestPackage("Morphir.Tool.*.nupkg");
        var packageVersion = GetPackageVersion(toolPackage);
    
        packageVersion.Should().Be(changelogVersion);
    }
    
    [Test]
    public async Task ToolPackage_HasCorrectMetadata()
    {
        var package = FindLatestPackage("Morphir.Tool.*.nupkg");
        var nuspec = GetNuspec(package);
    
        nuspec.Id.Should().Be("Morphir.Tool");
        nuspec.Authors.Should().Contain("FINOS");
        nuspec.License.Should().NotBeNullOrEmpty();
        nuspec.ProjectUrl.Should().Contain("morphir-dotnet");
        nuspec.PackageType.Should().Be("DotnetTool");
    }
    
    [Test]
    public async Task ToolPackage_HasReleaseNotes()
    {
        var package = FindLatestPackage("Morphir.Tool.*.nupkg");
        var nuspec = GetNuspec(package);
    
        nuspec.ReleaseNotes.Should().NotBeNullOrEmpty();
        nuspec.ReleaseNotes.Should().Contain("### "); // Has sections
    }
    

3.4 Local Installation Tests (Phase 2 of testing strategy)

  • Create LocalInstallationTests.cs:
    [Test]
    public async Task ToolPackage_InstallsFromLocalFolder()
    {
        // Arrange
        var tempDir = CreateTempDirectory();
        var localSource = Path.Combine(tempDir, "feed");
        Directory.CreateDirectory(localSource);
    
        var toolPackage = FindLatestPackage("Morphir.Tool.*.nupkg");
        File.Copy(toolPackage, Path.Combine(localSource, Path.GetFileName(toolPackage)));
    
        // Act
        var installResult = await RunDotNet(
            $"tool install --global --add-source {localSource} Morphir.Tool"
        );
    
        // Assert
        installResult.ExitCode.Should().Be(0);
    
        var versionResult = await RunDotNet("morphir --version");
        versionResult.ExitCode.Should().Be(0);
        versionResult.Output.Should().MatchRegex(@"\d+\.\d+\.\d+");
    
        // Cleanup
        await RunDotNet("tool uninstall --global Morphir.Tool");
        Directory.Delete(tempDir, true);
    }
    
    [Test]
    public async Task ToolCommand_IsAvailableAfterInstall()
    {
        // Assumes tool is installed
        var result = await RunCommand("morphir", "--help");
    
        result.ExitCode.Should().Be(0);
        result.Output.Should().Contain("morphir");
        result.Output.Should().Contain("Commands:");
    }
    

3.5 Add TestBuild Target

  • Create target in Build.Testing.cs:
    Target TestBuild => _ => _
        .DependsOn(PackAll)
        .Description("Run build system tests")
        .Executes(() =>
        {
            DotNetTest(s => s
                .SetProjectFile(RootDirectory / "tests" / "Morphir.Build.Tests" / "Morphir.Build.Tests.csproj")
                .SetConfiguration(Configuration)
                .EnableNoRestore()
                .EnableNoBuild());
        });
    
    Target TestAll => _ => _
        .DependsOn(Test, TestE2E, TestBuild)
        .Description("Run all tests");
    

3.6 Integrate into CI

  • Update .github/workflows/development.yml:
    - name: Run build tests
      run: ./build.sh TestBuild
    

BDD Tests:

Feature: Build testing infrastructure
  Scenario: Validate tool package structure
    Given Morphir.Tool package is built
    When I run package structure tests
    Then all required files should be present
    And tool settings should be correct

  Scenario: Validate version consistency
    Given all packages are built
    When I run metadata tests
    Then all packages should have same version
    And version should match CHANGELOG.md

  Scenario: Test local installation
    Given tool package is in local folder
    When I install tool from local source
    Then installation should succeed
    And morphir command should be available

Phase 4: Deployment & Distribution (2-3 days)

Goal: Update workflows for dual distribution

Tasks

4.1 Update Deployment Workflow

  • Update .github/workflows/deployment.yml:
    name: Deployment
    
    on:
      push:
        tags:
          - 'v*'  # Trigger on version tags (e.g., v0.2.1)
      workflow_dispatch:
        inputs:
          release_version:
            description: 'Version to deploy (optional, reads from CHANGELOG if not provided)'
            required: false
    
    jobs:
      validate-version:
        runs-on: ubuntu-latest
        outputs:
          version: ${{ steps.get-version.outputs.version }}
        steps:
          - uses: actions/checkout@v4
    
          - name: Get version from CHANGELOG
            id: get-version
            run: |
              # Extract from tag name (v0.2.1 → 0.2.1)
              if [[ "${{ github.ref }}" == refs/tags/* ]]; then
                VERSION=${GITHUB_REF#refs/tags/v}
                echo "version=$VERSION" >> $GITHUB_OUTPUT
              elif [[ -n "${{ github.event.inputs.release_version }}" ]]; then
                echo "version=${{ github.event.inputs.release_version }}" >> $GITHUB_OUTPUT
              else
                echo "No version specified"
                exit 1
              fi
    
          - name: Validate version in CHANGELOG
            run: |
              VERSION=${{ steps.get-version.outputs.version }}
              if ! grep -q "\[$VERSION\]" CHANGELOG.md; then
                echo "Version $VERSION not found in CHANGELOG.md"
                exit 1
              fi
    
      build-executables:
        needs: validate-version
        # ... existing build-executables jobs ...
    
      release:
        needs: [validate-version, build-executables]
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - name: Setup .NET SDK
            uses: actions/setup-dotnet@v4
            with:
              global-json-file: global.json
    
          - name: Restore dependencies
            run: ./build.sh Restore
    
          - name: Build
            run: ./build.sh Compile
    
          - name: Run tests
            run: ./build.sh TestAll  # Includes build tests!
    
          - name: Download executables
            uses: actions/download-artifact@v4
    
          - name: Pack packages
            run: ./build.sh PackAll
    
          - name: Run build tests
            run: ./build.sh TestBuild
    
          - name: Publish to NuGet
            run: ./build.sh PublishAll --api-key ${{ secrets.NUGET_TOKEN }}
            env:
              NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
    
      create-github-release:
        needs: [validate-version, build-executables, release]
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - name: Download executables
            uses: actions/download-artifact@v4
            with:
              path: artifacts/executables
    
          - name: Extract release notes from CHANGELOG
            id: release-notes
            run: |
              VERSION=${{ needs.validate-version.outputs.version }}
              # Extract section for this version from CHANGELOG.md
              awk '/## \['$VERSION'\]/,/## \[/ {print}' CHANGELOG.md | head -n -1 > release-notes.md
    
          - name: Create GitHub Release
            uses: softprops/action-gh-release@v1
            with:
              tag_name: v${{ needs.validate-version.outputs.version }}
              name: Release v${{ needs.validate-version.outputs.version }}
              body_path: release-notes.md
              files: |
                artifacts/executables/morphir-*
                artifacts/executables/morphir.exe
            env:
              GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    

4.2 Update Install Scripts

  • Verify scripts/install-linux.sh uses “morphir” command
  • Verify scripts/install-macos.sh uses “morphir” command
  • Verify scripts/install-windows.ps1 uses “morphir” command
  • Update download URLs to point to GitHub releases:
    VERSION="0.2.1"
    URL="https://github.com/finos/morphir-dotnet/releases/download/v${VERSION}/morphir-linux-x64"
    
  • Test install scripts locally (manual)

4.3 Validate PublishTool Target

  • Update glob pattern in Build.Publishing.cs:
    Target PublishTool => _ => _
        .DependsOn(PackTool)
        .Description("Publish Morphir.Tool to NuGet.org")
        .Executes(() =>
        {
            if (string.IsNullOrEmpty(ApiKey))
                throw new Exception("API_KEY required");
    
            var toolPackage = OutputDir.GlobFiles("Morphir.Tool.*.nupkg")
                .FirstOrDefault();
    
            if (toolPackage == null)
                throw new Exception($"Morphir.Tool package not found in {OutputDir}");
    
            Serilog.Log.Information($"Publishing {toolPackage}");
    
            DotNetNuGetPush(s => s
                .SetTargetPath(toolPackage)
                .SetSource(NuGetSource)
                .SetApiKey(ApiKey)
                .SetSkipDuplicate(true));
        });
    

BDD Tests:

Feature: Deployment workflow
  Scenario: Deploy on tag push
    Given tag v0.2.1 is pushed
    When deployment workflow runs
    Then version should be extracted from CHANGELOG.md
    And packages should be built
    And build tests should run
    And packages should be published to NuGet
    And executables should be uploaded to GitHub release

  Scenario: Block deployment if version not in CHANGELOG
    Given tag v0.2.2 is pushed
    But CHANGELOG.md doesn't have [0.2.2]
    When deployment workflow runs
    Then workflow should fail
    And no packages should be published

Phase 5: Documentation (1-2 days)

Goal: Comprehensive documentation for all stakeholders

Tasks

5.1 Update AGENTS.md

  • Add section: “Build System Configuration”

    ## Build System Configuration
    
    ### Nuke Parameters
    
    The build system uses Nuke with these parameters:
    
    - `--configuration`: Build configuration (Debug/Release)
    - `--version`: Version override (reads from CHANGELOG.md by default)
    - `--api-key`: NuGet API key for publishing
    - `--nuget-source`: NuGet source URL
    - `--skip-tests`: Skip test execution
    
    ### Environment Variables
    
    - `NUGET_TOKEN`: NuGet API key (CI only)
    - `CONFIGURATION`: Build configuration override
    - `MORPHIR_TEST_TARGET`, `MORPHIR_TEST_EXECUTABLE`: the target of the black-box tests and the path of an executable
    
  • Add section: “Changelog-Driven Versioning”

    ## Changelog-Driven Versioning
    
    Morphir uses CHANGELOG.md as the single source of truth for versions.
    
    ### Version Format
    
    Follows [Semantic Versioning](https://semver.org/):
    - `MAJOR.MINOR.PATCH` for releases (e.g., `0.2.1`)
    - `MAJOR.MINOR.PATCH-TYPE.NUMBER` for pre-releases (e.g., `0.2.1-beta.2`)
    
    Supported pre-release types: alpha, beta, preview, rc
    
    ### Release Preparation Workflow
    
    1. During development, add changes to `[Unreleased]` section
    2. When ready to release, run: `./build.sh PrepareRelease --version X.Y.Z`
    3. Review staged changes: `git diff --staged`
    4. Commit: `git commit -m "chore: prepare release X.Y.Z"`
    5. Create release branch: `git checkout -b release/X.Y.Z`
    6. Push and create PR to main
    7. After PR merge, create tag: `git tag -a vX.Y.Z -m "Release X.Y.Z"`
    8. Push tag: `git push origin vX.Y.Z` (triggers deployment)
    
  • Add section: “Dual Distribution Strategy”

    ## Dual Distribution Strategy
    
    Morphir provides two distribution channels:
    
    ### NuGet Tool Package (Morphir.Tool)
    
    **For**: .NET developers with SDK installed
    **Install**: `dotnet tool install -g Morphir.Tool`
    **Update**: `dotnet tool update -g Morphir.Tool`
    **Command**: `morphir`
    
    ### Platform Executables
    
    **For**: Shell scripts, containers, non-.NET environments
    **Install**: Use install scripts or download from GitHub releases
    **Platforms**: linux-x64, linux-arm64, win-x64, osx-arm64
    **Command**: `morphir` or `./morphir-{platform}`
    

5.2 Update CLAUDE.md

  • Add build organization guidance
  • Document PrepareRelease workflow
  • Add testing requirements
  • Update commit message examples

5.3 Update README.md

  • Add persona-based installation instructions:

    ## Installation
    
    ### For .NET Developers
    
    If you have the .NET SDK installed:
    
    ```bash
    dotnet tool install -g Morphir.Tool
    morphir --version
    

    For Shell Scripts / Containers

    If you don’t have .NET SDK or need a standalone executable:

    Linux/macOS:

    curl -sSL https://get.morphir.org | bash
    

    Windows:

    irm https://get.morphir.org/install.ps1 | iex
    

    Manual Download: Download from GitHub Releases

5.4 Create DEPLOYMENT.md

  • Document release process for maintainers
  • Add troubleshooting guide
  • Document rollback procedures
  • Add deployment checklist

5.5 Write BDD Feature Files

  • Create tests/Morphir.BlackBox.Tests/EndToEnd/ToolInstallation.feature:

    Feature: Morphir Tool Installation
      As a .NET developer
      I want to install Morphir as a dotnet tool
      So that I can use it in my development workflow
    
      Scenario: Install from NuGet
        Given I am a .NET developer with SDK installed
        When I run "dotnet tool install -g Morphir.Tool"
        Then the tool should install successfully
        And I should be able to run "morphir --version"
        And the version should match CHANGELOG.md
    
      Scenario: Update tool
        Given Morphir.Tool is already installed
        When I run "dotnet tool update -g Morphir.Tool"
        Then the tool should update successfully
        And the new version should be active
    
  • Create tests/Morphir.BlackBox.Tests/EndToEnd/ExecutableDownload.feature:

    Feature: Morphir Executable Download
      As a shell script user
      I want to download a standalone executable
      So that I can use Morphir without installing .NET SDK
    
      Scenario: Download from GitHub releases
        Given I am using a minimal container
        When I download morphir-linux-x64 from GitHub releases
        Then I should be able to run "./morphir-linux-x64 --version"
        And the version should match CHANGELOG.md
    
      Scenario: Install via script
        Given I have curl available
        When I run the install script
        Then morphir should be installed to /usr/local/bin
        And morphir command should be in PATH
    

BDD Tests:

Feature: Documentation completeness
  Scenario: All distribution methods documented
    Given README.md exists
    When I read installation instructions
    Then I should see dotnet tool installation
    And I should see executable download instructions
    And I should see persona-based recommendations

  Scenario: Release process documented
    Given AGENTS.md exists
    When I read the release preparation section
    Then I should see PrepareRelease workflow
    And I should see tag creation steps
    And I should see deployment trigger explanation

BDD Acceptance Criteria

Epic-Level Scenarios

Feature: Morphir Deployment Architecture
  As a Morphir maintainer
  I want a robust deployment architecture
  So that releases are reliable and users can install easily

Background:
  Given the morphir-dotnet repository is up to date
  And all dependencies are installed

Scenario: Successful deployment to NuGet and GitHub
  Given CHANGELOG.md has [0.2.1] - 2025-12-20
  And all changes are committed
  When I create and push tag v0.2.1
  Then deployment workflow should complete successfully
  And Morphir.Tool.0.2.1.nupkg should be published to NuGet.org
  And Morphir.Core.0.2.1.nupkg should be published to NuGet.org
  And Morphir.Tooling.0.2.1.nupkg should be published to NuGet.org
  And morphir-linux-x64 should be in GitHub release v0.2.1
  And morphir-win-x64 should be in GitHub release v0.2.1
  And morphir-osx-arm64 should be in GitHub release v0.2.1
  And release notes should match CHANGELOG.md

Scenario: Build tests catch package issues
  Given I modify package structure incorrectly
  When I run "./build.sh TestBuild"
  Then tests should fail
  And I should see clear error message
  And CI deployment should be blocked

Scenario: Version consistency across packages
  Given I prepare release 0.2.1
  When I build all packages
  Then all packages should have version 0.2.1
  And version should match CHANGELOG.md [0.2.1]
  And all package release notes should match

Scenario: .NET developer installation
  Given Morphir.Tool is published to NuGet
  When .NET developer runs "dotnet tool install -g Morphir.Tool"
  Then tool should install successfully
  And "morphir --version" should work
  And version should match published version

Scenario: Container user installation
  Given morphir-linux-x64 is in GitHub releases
  When container user downloads executable
  Then "./morphir-linux-x64 --version" should work
  And version should match release version
  And no .NET SDK should be required

Component-Level Scenarios

See individual phase BDD tests in Implementation Plan sections.


Testing Strategy

Test Pyramid

         /\
        /E2E\        Black-box Tests (Morphir.BlackBox.Tests)
       /______\      - Full tool installation workflows
      /        \     - Executable download and usage
     / Integration\  - Cross-platform verification
    /______________\
   /                \
  /   Unit Tests     \ Unit Tests (Morphir.Build.Tests)
 /____________________\ - Package structure validation
                        - Metadata correctness
                        - Version extraction
                        - Changelog parsing

Test Categories

1. Build System Tests (tests/Morphir.Build.Tests/)

Package Structure Tests:

  • Validate tool package contains correct files
  • Validate library packages contain correct files
  • Validate DotnetToolSettings.xml correctness
  • Validate no unnecessary files included

Package Metadata Tests:

  • Version consistency across packages
  • Version matches CHANGELOG.md
  • Authors, license, URLs set correctly
  • Release notes extracted correctly
  • PackageId naming conventions

Changelog Tests:

  • Parse valid changelog
  • Extract version correctly
  • Extract release notes correctly
  • Validate unreleased content detection
  • Test PrepareRelease transformations

Local Installation Tests (Phase 2):

  • Install tool from local folder
  • Verify command is available
  • Run –version and validate output
  • Uninstall successfully

2. Black-box Tests (tests/Morphir.BlackBox.Tests/)

Tool Installation Tests:

  • Install from NuGet feed
  • Update tool
  • Uninstall tool
  • Verify command availability

Executable Tests:

  • Download from GitHub releases
  • Execute on each platform
  • Verify version output
  • Test basic commands

Cross-Platform Tests:

  • Linux x64
  • Linux ARM64
  • Windows x64
  • macOS ARM64

3. Integration Tests

CI Workflow Tests (manual verification):

  • Tag push triggers deployment
  • Version validation passes
  • Build tests run successfully
  • Packages publish to NuGet
  • GitHub release created with executables

Install Script Tests (manual verification):

  • Linux install script works
  • macOS install script works
  • Windows install script works
  • Scripts download correct version

Coverage Targets

  • Build System: >= 80% code coverage
  • Unit Tests: >= 80% code coverage (existing requirement)
  • E2E Tests: All critical user journeys covered
  • Manual Tests: Release checklist 100% complete

CI Integration

# .github/workflows/development.yml
jobs:
  test:
    steps:
      - name: Run unit tests
        run: ./build.sh Test

      - name: Run build tests
        run: ./build.sh TestBuild

      - name: Run E2E tests
        run: ./build.sh TestE2E

# .github/workflows/deployment.yml
jobs:
  release:
    steps:
      - name: Run all tests
        run: ./build.sh TestAll  # Blocks deployment if tests fail

Risks & Mitigation

Risk 1: Version Drift Between Packages

Risk: Different packages published with different versions

Impact: HIGH - User confusion, installation failures

Probability: LOW (after mitigation)

Mitigation:

  • ✅ Single source of truth (CHANGELOG.md)
  • ✅ Automated extraction via Ionide.KeepAChangelog
  • ✅ Build tests validate version consistency
  • ✅ CI blocks if versions don’t match

Detection: Build tests fail, CI blocks deployment

Recovery: Fix CHANGELOG.md, rebuild packages


Risk 2: Breaking Existing Users

Risk: Users with current tool/executable can’t upgrade

Impact: MEDIUM - User frustration, support burden

Probability: LOW

Mitigation:

  • ✅ Keep backward compatibility (command name stays “morphir”)
  • ✅ Clear migration documentation
  • ✅ Test installation on clean machines
  • ✅ Announce changes in release notes

Detection: User reports, E2E tests

Recovery: Hotfix release, update documentation


Risk 3: Build Tests Add CI Time

Risk: CI takes longer, slows development

Impact: LOW - Developer velocity

Probability: MEDIUM

Mitigation:

  • ✅ Run build tests in parallel with other tests
  • ✅ Cache NuGet packages
  • ✅ Optimize test execution
  • ✅ Phase 2/3 tests are optional (local only)

Measurement: Monitor CI duration, target < 10 minutes total


Risk 4: Complex Release Process

Risk: Release preparation is error-prone

Impact: MEDIUM - Release delays

Probability: LOW (after automation)

Mitigation:

  • ✅ Automated PrepareRelease target
  • ✅ Clear documentation and checklists
  • ✅ Validation steps prevent mistakes
  • ✅ Dry-run capability

Detection: PrepareRelease validation failures

Recovery: Fix issues, re-run PrepareRelease


Risk 5: Ionide.KeepAChangelog Bugs

Risk: Parser fails or extracts incorrect version

Impact: HIGH - Deployment failure

Probability: VERY LOW (mature library)

Mitigation:

  • ✅ Comprehensive changelog validation tests
  • ✅ Fallback to manual version override
  • ✅ CI validation before deployment
  • ✅ Monitor parser errors

Detection: Build tests, CI validation

Recovery: Manual version override, report bug upstream


Success Metrics

Immediate Success Criteria (Phase 1-2)

  • Zero deployment failures due to package naming
  • All packages have same version from CHANGELOG.md
  • Build tests catch 100% of package structure issues
  • PrepareRelease target works without errors
  • Documentation covers all user personas

Short-Term Success Criteria (Phase 3-4)

  • CI deployment time < 10 minutes
  • Build tests have >= 80% coverage
  • GitHub releases created automatically
  • Install scripts work on all platforms
  • Zero user-reported installation issues

Long-Term Success Criteria (6 months)

  • Deployment success rate >= 99%
  • Release preparation time < 5 minutes
  • User satisfaction with installation >= 90%
  • Build system maintainability score >= 8/10
  • Zero security vulnerabilities in packages

Key Performance Indicators (KPIs)

Reliability:

  • Deployment success rate
  • Build test pass rate
  • Package validation pass rate

Efficiency:

  • Average release preparation time
  • CI execution time
  • Time to fix deployment issues

Quality:

  • Package structure defects found
  • Version consistency violations
  • User-reported installation issues

Developer Experience:

  • Time to understand release process
  • Number of manual steps required
  • Documentation completeness score

Timeline

Gantt Chart

Phase 1: Project Structure & Build Organization [3-4 days]
├─ Create Morphir.Tool project           [1 day]
├─ Split Build.cs by domain              [1 day]
├─ Create helper classes                 [0.5 day]
├─ Remove deprecated code                [0.5 day]
└─ Update build targets                  [1 day]

Phase 2: Changelog-Driven Versioning [2-3 days]
├─ Add Ionide.KeepAChangelog             [0.5 day]
├─ Implement version extraction          [0.5 day]
├─ Implement release notes extraction    [0.5 day]
├─ Implement PrepareRelease target       [1 day]
├─ Implement changelog manipulation      [0.5 day]
└─ Implement auto pre-release bumping    [0.5 day]

Phase 3: Build Testing Infrastructure [3-4 days]
├─ Create test project                   [0.5 day]
├─ Package structure tests               [1 day]
├─ Package metadata tests                [1 day]
├─ Local installation tests              [1 day]
├─ Add TestBuild target                  [0.5 day]
└─ Integrate into CI                     [0.5 day]

Phase 4: Deployment & Distribution [2-3 days]
├─ Update deployment workflow            [1 day]
├─ Create GitHub release automation      [1 day]
├─ Update install scripts                [0.5 day]
└─ Validate PublishTool target           [0.5 day]

Phase 5: Documentation [1-2 days]
├─ Update AGENTS.md                      [0.5 day]
├─ Update CLAUDE.md                      [0.5 day]
├─ Update README.md                      [0.5 day]
├─ Create DEPLOYMENT.md                  [0.5 day]
└─ Write BDD feature files               [0.5 day]

Total: 11-16 days

Milestones

M1: Core Architecture Complete (Day 4)

  • Morphir.Tool project created
  • Build.cs split and organized
  • Deprecated code removed
  • Build targets updated

M2: Version Management Complete (Day 7)

  • Ionide.KeepAChangelog integrated
  • PrepareRelease target working
  • CHANGELOG.md is single source of truth
  • Pre-release bumping implemented

M3: Testing Infrastructure Complete (Day 11)

  • Build tests project created
  • Package validation tests passing
  • Local installation tests passing
  • CI integration complete

M4: Deployment Ready (Day 14)

  • Deployment workflow updated
  • GitHub releases automated
  • Install scripts validated
  • End-to-end flow tested

M5: Documentation Complete (Day 16)

  • All documentation updated
  • BDD scenarios written
  • Release process documented
  • Ready for production release

Design Decision Rationale

Why Separate Projects?

Context: Single project tried to serve both tool and executable use cases.

Problem:

  • Mixed concerns (tool packaging + AOT compilation)
  • Complex build configuration
  • Difficult to optimize for each scenario
  • Package naming confusion

Decision: Create separate Morphir.Tool project

Reasoning:

  1. Industry pattern: Nuke.GlobalTool, GitVersion.Tool, etc.
  2. Clear boundaries: Tool project knows nothing about AOT
  3. Independent optimization: Tool package can be small, executable can be trimmed
  4. Easier testing: Can test tool installation separately from executable behavior
  5. Maintainability: Each project has single responsibility

Alternatives Rejected:

  • Keep single project: Doesn’t address root cause
  • Rename package only: Band-aid solution
  • Use complex build conditions: Too fragile

Why Ionide.KeepAChangelog?

Context: Need changelog-driven versioning with pre-release support.

Problem:

  • Manual version management is error-prone
  • CHANGELOG.md and versions can drift
  • Pre-release versions not standardized
  • GitVersion doesn’t fit changelog-first workflow

Decision: Use Ionide.KeepAChangelog as single source of truth

Reasoning:

  1. Respects Keep a Changelog: Already following this standard
  2. Full SemVer support: Pre-release versions (alpha, beta, rc) via Semver library
  3. Mature library: Used in F# ecosystem, well-tested
  4. Single source of truth: No version.json duplication
  5. Release notes automation: Automatically extract for packages

Alternatives Rejected:

  • version.json: Duplication with CHANGELOG.md
  • GitVersion: Doesn’t fit changelog-driven approach
  • Environment variable only: No validation, error-prone
  • FAKE.Core.Changelog: Requires FAKE build system

Why Hybrid Testing Strategy?

Context: Need to catch packaging issues before CI.

Problem:

  • No automated package validation
  • TestContainers adds complexity
  • Want fast feedback loop

Decision: Phase 1 (structure/metadata), Phase 2 (local install), Phase 3 (containers)

Reasoning:

  1. Pragmatic: Start simple, add complexity when needed
  2. Fast feedback: No Docker startup for basic validation
  3. Covers 80%: Structure/metadata tests catch most issues
  4. Incremental: Can add TestContainers later
  5. Low barrier: Easy for contributors to run

Alternatives Rejected:

  • Folder-based only: Insufficient validation
  • Full TestContainers immediately: Overkill, slower tests, complexity
  • No tests: Unacceptable, issues found in CI only

Why Split Build.cs by Domain?

Context: Build.cs will grow with new features.

Problem:

  • Single 900+ line file becomes unwieldy
  • Vertical slice architecture used in Morphir.Tooling
  • Want consistent patterns across codebase

Decision: Split by domain (Packaging, Publishing, Testing) + extract helpers

Reasoning:

  1. Aligns with architecture: Matches Morphir.Tooling vertical slices
  2. Clear boundaries: Related targets grouped together
  3. Scalable: Easy to add new domains (Documentation, Analysis)
  4. Testable: Helper classes can be unit tested
  5. Team familiarity: Same patterns they already use

Alternatives Rejected:

  • Keep single file: Will become unmaintainable
  • Split by technical concern: Doesn’t match feature boundaries
  • Vertical slice with separate files: Overkill for current size

Why Dual Distribution?

Context: Different users have different needs.

Problem:

  • .NET developers want dotnet tool
  • Container users can’t install .NET SDK
  • Shell scripts need standalone executable

Decision: Publish both tool package and executables

Reasoning:

  1. Serves all personas: No user excluded
  2. Industry standard: How major tools distribute
  3. Optimal for each: Tool for dev, AOT for production
  4. Flexible: Users choose what works for them
  5. Already building both: Just need to organize/document

Alternatives Rejected:

  • Tool only: Excludes non-SDK users
  • Executable only: Not idiomatic for .NET developers
  • Force users to choose one: Why limit options?

References

Internal Documents

External References

Morphir:

Standards:

Tools:

Patterns:


Appendices

Appendix A: Current vs Future Package Structure

Current (Broken):

artifacts/packages/
├── morphir.0.2.0.nupkg           ← lowercase (breaks glob)
├── Morphir.Core.0.2.0.nupkg
└── Morphir.Tooling.0.2.0.nupkg

Future (Fixed):

artifacts/packages/
├── Morphir.Tool.0.2.1.nupkg      ← New, capital M
├── Morphir.Core.0.2.1.nupkg
└── Morphir.Tooling.0.2.1.nupkg

artifacts/executables/
├── morphir-linux-x64             ← Standalone
├── morphir-linux-arm64
├── morphir-win-x64
└── morphir-osx-arm64

Appendix B: CHANGELOG.md Format Examples

Valid pre-release entries:

## [0.2.1-alpha.1] - 2025-12-18
## [0.2.1-alpha.2] - 2025-12-19
## [0.2.1-beta.1] - 2025-12-20
## [0.2.1-beta.2] - 2025-12-21
## [0.2.1-rc.1] - 2025-12-22
## [0.2.1] - 2025-12-23

Invalid entries:

## [0.2.1-SNAPSHOT] - 2025-12-18  ❌ Not SemVer
## [0.2.1-beta] - 2025-12-19      ⚠️ Missing number (but parses)
## 0.2.1 - 2025-12-20              ❌ Missing brackets
## [0.2.1]                         ❌ Missing date

Appendix C: Build Target Dependency Graph

CI (full pipeline)
├── Restore
├── Compile
│   └── Restore
├── Test
│   └── Compile
├── TestE2E
│   └── Compile
├── PackAll
│   ├── PackLibs
│   │   └── Compile
│   └── PackTool
│       └── Compile
├── TestBuild
│   └── PackAll
└── PublishAll
    ├── PublishLibs
    │   └── PackLibs
    └── PublishTool
        └── PackTool

PrepareRelease (standalone)
└── (validates CHANGELOG.md)

BumpPreRelease (CI only)
└── (updates CHANGELOG.md)

Appendix D: File Size Estimates

Tool Package (~5-10 MB):

  • Managed DLLs only
  • Dependencies: Morphir.Core, Morphir.Tooling, WolverineFx, etc.
  • No native code

Executables (~50-80 MB each):

  • AOT-compiled native code
  • Self-contained (no .NET SDK required)
  • Trimmed and optimized
  • Platform-specific

Comparison:

  • NuGet tool: Fast download for developers with SDK
  • Executables: Larger but work everywhere

Appendix E: Version Comparison Matrix

ScenarioCurrentFuture
Version sourceRELEASE_VERSION env varCHANGELOG.md
Pre-releaseManual stringSemVer (alpha.1, beta.2)
ValidationNoneAutomated (build tests)
ConsistencyManual verificationEnforced (single source)
Release notesManual copy-pasteAuto-extracted
Drift riskHIGHLOW

Status Tracking

Feature Status

FeatureStatusNotes
Morphir.Tool project⏳ PlannedPhase 1
Build.cs split⏳ PlannedPhase 1
Helper classes⏳ PlannedPhase 1
Ionide.KeepAChangelog⏳ PlannedPhase 2
PrepareRelease target⏳ PlannedPhase 2
Build tests project⏳ PlannedPhase 3
Package validation⏳ PlannedPhase 3
Deployment workflow⏳ PlannedPhase 4
GitHub releases⏳ PlannedPhase 4
Documentation⏳ PlannedPhase 5

Blockers

BlockerImpactResolution
None yet--

Decisions Pending

DecisionOptionsStatus
None-All approved

PRD Version: 1.0 Last Updated: 2025-12-18 Status: Approved Owner: @morphir-maintainers

16.1.6.5 - PRD: Layered Configuration

Shared configuration, project discovery, and configuration inspection and editing

Layered configuration and workspaces

Status: In Progress; campaigns 1 through 5 landed and cleaned; campaign 6 workspace migration in progress under #479

Created: 2025-12-22

Last updated: 2026-11-03

Overview

Provide one reusable Morphir configuration and project system for the CLI and other hosts. Read TOML, YAML, JSON, and JSON5; discover projects and declared workspaces; and resolve settings with provenance. Shared APIs support morphir config inspection/editing, morphir project migrate for one classic project, and morphir workspace migrate for coordinated workspace adoption.

The original cache-only TOML implementation keeps its Git-first compatibility behavior. Campaign 1 added typed multi-format documents, normalization, resolution, named profiles, validation, and provenance. Campaign 2 adds document-aware discovery through separate library entry points. Campaign 3 adds inspection and standard .NET consumption. Campaign 4 added shared editing and TOML transactions. Campaign 5 added loss-checked classic project migration. Campaign 6 implements bounded workspace migration and explicit transaction recovery.

Compatibility and decisions

Use the MoonBit configuration specification and adoption decision for discovery and layering. Use the finos/morphir configuration model for shared fields. MoonBit’s format layering supersedes the older core specification’s refusal of coexisting TOML and YAML.

The guide describes target rules. The CLI reference records implemented inspection and editing interfaces.

Goals

  1. Share core functionality across hosts without a dependency on the CLI.
  2. Match MoonBit discovery, precedence, legacy manifest normalization, and project selection.
  3. Preserve typed values and winning origins.
  4. Integrate with Microsoft.Extensions.Configuration through an adapter.
  5. Support scoped inspection and validated TOML editing.
  6. Document current support separately from planned behavior.
  7. Migrate classic morphir.json projects and adopt repositories into workspaces without silently losing settings or changing project behavior.

Non-goals

  • Implementing a task runner, compiler, cache store, or every consumer of configuration fields.
  • Writing YAML/JSON configuration or silently converting existing files.
  • Reproducing Git’s complete command set or multi-valued INI semantics.
  • Executing scripts or resolving secrets during inspection.

User stories

  • A developer invokes a tool from a member’s source directory and finds its declared workspace.
  • A repository retains identity in morphir.json while TOML overrides compiler settings.
  • A developer explicitly migrates a classic manifest to TOML, previews the changes, and checks that the new configuration works independently of the old file.
  • A repository maintainer adopts several classic projects through one bounded workspace plan while preserving existing membership and settings.
  • A developer inspects why a setting wins and edits a specific project, workspace, personal, or machine scope.
  • An IDE or embedded host loads the same configuration without invoking the CLI.
  • A CI job obtains valid JSON output and selects profiles explicitly.

Functional requirements

Discovery and project system

  • Inspect root, hidden, and dot-config primary stems with all five extensions. Include supplemental morphir-workspace.json below ordinary JSON primaries.
  • Parse primaries during the upward walk. Remember the nearest project, but stop at the nearest declared workspace. Respect host boundaries. Git or empty .morphir directories do not declare workspaces.
  • Merge JSON/JSON5, then YAML/YML, then TOML. Reject multiple candidates in one format group across layouts. Apply the same rules to members and eligible overrides/profiles.
  • Expand workspace.members, apply workspace.exclude, respect workspace.root and workspace.default_member, and distinguish virtual workspaces from root projects.
  • Support explicit path/name and invocation-based project selection. Refuse multi-project edit targets.
  • Use canonical snake-case fields, including project.source_directory and workspace.out_dir. Diagnose member attempts to relocate the workspace output root.

Resolution

  • Apply defaults, system, global, root primaries, selected file, member primaries, active profiles, adjacent user overrides, environment, and explicit CLI values in that order.
  • Preserve lower-format-only keys; recursively merge maps; replace complete arrays and incompatible shapes; treat secret references as leaves.
  • Retain typed documents, winning leaf/array provenance, source reports, and structured diagnostics. Preserve unknown settings allowed by the shared model.
  • Exclude whole project and workspace subtrees from system/global defaults and from root-to-member document inheritance. Supply owning workspace topology/output-root context separately.
  • Normalize legacy morphir-elm fields and reject same-file legacy/canonical conflicts. Report unsupported fields.
  • Implement OS-specific system/global paths, MORPHIR_HOME, same-format ambiguity across alternate global locations, and isolated loading.
  • Use __ for environment nesting. Match upstream typing and deterministic scalar/nested-key conflict behavior.
  • Implement named profiles, explicit disabling, base-only controls, includes/groups, active-name order, and workspace-before-member files. Inactive files are not loaded. No implicit CI or default profile is active.
  • Report upstream mixed-format warning codes. Only host controls and system/global settings may suppress them.

Inspection and editing

  • Expose get, list, show, path, explain, set, and unset under morphir config.
  • Reads default to effective configuration. Scope selectors limit source inspection; origin/scope options expose the winner. explain reports contributing layers and the winner.
  • Writes target one project, workspace, adjacent user, global, system, or explicit TOML file. Resolve an unambiguous current project/workspace by default. Do not write implicitly to an environment or profile source just because it supplies the winning value.
  • Support explicit strings, booleans, integers, numbers, arrays, and objects. Separate input typing from JSON output.
  • Preserve TOML formatting/comments. Create the corresponding TOML primary when only lower-format files exist. Write full replacement arrays/objects when those values are set.
  • unset removes only the target entry; inherited values may reappear. Report the resulting effective value and whether an edited value remains overridden.
  • Validate before applying; install writes atomically; detect concurrent changes; leave files unchanged on validation or conflict failures.
  • Redact sensitive values. Return structured results for non-CLI hosts.

Classic project migration

  • Provide a reusable migration planner and apply operation, exposed by the planned morphir project migrate [<path>] command. The path accepts one project directory or classic manifest. This migrates project configuration, not morphir-ir.json or IR versions.
  • Accept a classic morphir-elm morphir.json; use the current unambiguous project’s classic manifest when the source is omitted. Do not implicitly migrate every workspace member.
  • Default to adjacent morphir.toml. An explicit output may select a supported primary layout within the same project location. Refuse targets that introduce same-format ambiguity or change project-relative path meaning.
  • Map name, sourceDirectory, and exposedModules to their canonical project fields. Materialize legacy-derived frontend/pipeline defaults, including Elm and checkpoint behavior, wherever needed for the new file to retain behavior without the classic manifest.
  • Preserve supported additional settings and relative-path meaning. Diagnose every unmapped or unrepresentable field, including legacy dependency/decorations shapes. Block application when any value would be lost or its meaning cannot be preserved; a reader’s warning-and-ignore policy is not sufficient for migration.
  • Reject same-file legacy/canonical conflicts and conflicting existing TOML values. Add nonconflicting missing values to an existing target while preserving unrelated settings, comments, and formatting. Identical existing values are unchanged; a repeat migration is a no-op.
  • Provide a read-only --dry-run that reports source, target, field mappings, planned edits, conflicts, and verification results. Redact sensitive values in previews and JSON reports.
  • Before applying, compare project-scoped resolution before migration with resolution of the candidate with the legacy source excluded. Exclude ambient machine, personal, environment, CLI, and activated-profile overlays from this comparison so they cannot hide a change; keep relevant declared workspace context. Fail if values or path meaning differ. Provenance paths may change and must be reported, not treated as value differences.
  • Keep the classic source unchanged. Removing it is a separate deliberate action after users have migrated any consumers that still require morphir.json. Apply only verified edits atomically, detecting changes to source, target, and relevant workspace context. Validation or stale-plan failures leave both files unchanged; incomplete write recovery must report affected paths and retain recovery data.

Workspace migration

  • Expose morphir workspace migrate [<directory>] through shared bounded inventory, planning, and application services, including when no workspace declaration exists yet.
  • Inventory only the selected root. Respect include/exclude patterns and existing declared exclusions; skip discovered links, nested declared workspaces, and version-control/dependency/build-output directories.
  • Propose explicit root-relative membership, preserve existing settings/defaults and already migrated members, and distinguish root projects from virtual workspaces. Report duplicate names without conflating project paths.
  • Reuse project migration plans and their loss checks. Verify classic-to-TOML equivalence in the same proposed workspace context, preserving project source-path meaning and settings. Report topology, member/default-selection, and shared-output-root changes separately as intended adoption effects; no generated files are moved. Verify the aggregate workspace against those declared effects. A blocked selected project blocks the batch.
  • Support read-only --dry-run and JSON reports listing the full inventory, exclusions, edits, blockers, and verification outcomes.
  • Validate and stage the entire plan before replacing destinations. Coordinate overlapping writers, recheck preconditions, and journal replacements with rollback/recovery reporting. Do not claim crash-atomic multi-file writes. Retain all classic sources and unresolved recovery evidence; successful or fully rolled-back operations clean temporary artifacts.
  • Equivalent repeats are no-ops. A valid existing workspace with nothing to convert succeeds unchanged; an undeclared root without eligible projects reports no adoption plan.

Delivery campaigns

Each campaign is one cohesive implementation change delivered through one reviewable PR. Start dependent campaigns from the verified main result of their prerequisites rather than accumulating an unmerged branch stack. Tests and documentation land with the behavior they describe.

OrderOpenSpec changeOutcomePrerequisites
1add-shared-configurationTyped documents, format readers, resolution, profiles, provenance, and validation shared by hostsNone
2add-project-workspace-discoveryBounded roots, member discovery/selection, and applicable sources1
3add-config-inspectionShared queries, read-only config commands, and opt-in Microsoft configuration adapter1, 2
4add-config-editingShared TOML edit planning/application and config set/unset3
5add-classic-project-migrationShared loss-checked migration and project migrate2, 4
6add-workspace-migrationBounded repository adoption and coordinated workspace migrate5

Planning artifacts are under openspec/changes/<change-name>/, or their dated archive after implementation. Campaigns 1 through 5 are landed and cleaned. Campaign 3 tracks issue #468; its delivery journal records shared queries, CLI reads, Microsoft integration and verified landing/cleanup evidence. Campaign 4 tracks issue #473, landed through PR #475 at 56d9de917f6c3035c8a2f994f49dfc96294619d8 with parent cleanup confirmed. Its delivery journal and completion receipt retain qualification evidence. Campaign 5 tracks issue #476; its delivery journal records implementation and validation. PR #477 landed campaign 5 at e3b9a053120a49e94c70c30cd4f62ef783672c64; its completion receipt confirms review, checks and owned cleanup. Campaign 6 tracks #479 and its delivery journal.

Campaign completion

A campaign is complete only when all of these outcomes are verified:

  1. Its implementation, tests, and docs are in a reviewable PR; required checks and reviews pass on the delivered head.
  2. Its OpenSpec changes are archived and the main specs are synchronized coherently with the delivered implementation.
  3. The PR is merged to main, and authoritative merge evidence identifies a merged commit reachable from the updated main branch.
  4. The campaign-owned worktree is clean and contains no sole copy of unmerged work. Its worktree and local/remote feature branches are removed after verifying the merged state.
  5. The completion receipt records the PR URL, merged SHA, validation/review evidence, archive location, and worktree/branch cleanup verification.

Finishing code or obtaining green checks is not campaign completion. Cleanup is limited to artifacts owned by that campaign. Preserve unrelated branches/worktrees, unmerged changes, and recovery data; do not use forced removal or broad pruning to manufacture a clean result. Pending recovery or cleanup remains outstanding work and must be reported. Follow repository merge authorization and protection rules; this plan does not imply admin bypass or force-push permission.

Architecture

Core behavior belongs to reusable libraries. Morphir.Configuration is the existing home for immutable models and pure rules. Discovery/resolution services must be callable without the CLI; file, directory, environment, and write access are supplied through adapters. A host can provide native filesystem access or an in-memory/mounted tree.

A Microsoft configuration adapter exposes the resolved model through the proposed AddMorphirConfiguration(...). The typed document remains authoritative because flattened strings cannot alone preserve array, key, type, and provenance semantics. Editing uses explicit shared services.

The CLI handles arguments, output, and exit codes. Library services handle queries, explanations, edit and migration planning, validation, equivalence checks, and application. Other hosts can preview and apply the same migration without invoking the CLI. Keep Native AOT support through explicit/source-generated binding. Final package names and signatures remain to be designed.

Documentation acceptance criteria

  • A guide explains formats, layouts, boundaries, discovery, ambiguity, precedence, membership, profiles, environment values, legacy fields, and host integration.
  • A CLI reference covers every subcommand, selectors, typed inputs, output, exit codes, and write semantics.
  • A walkthrough demonstrates a workspace/member, inspection, personal overrides, inheritance after unset, and CI inspection.
  • Project and workspace migration references include before/after files, dry-run/apply commands, bounded inventory, existing-target conflicts, unsupported fields, source retention, and verification independent of legacy files.
  • Delivery tracking records each campaign’s landed PR, synchronized/archive specs, and campaign-owned worktree/branch cleanup.
  • Troubleshooting covers wrong roots, ambiguity, inactive profiles, overridden edits, revealed lower layers, and write conflicts.
  • CLI, guide, getting-started, and PRD indexes link to these pages.
  • Planned examples stay labeled until implemented and verified. Update examples and status with implementation.

Verification strategy

Follow red-green-refactor for implementation. Start with BDD scenarios and focused behavioral tests using upstream compatibility fixtures where possible.

  • Discovery: every layout/extension, nested workspaces, fallback, boundaries, input files, absent primaries, and same-group ambiguities.
  • Resolution: each precedence boundary, types, complete/empty array replacement, shape replacement, casing, origins, environment conflicts, and legacy mappings.
  • Projects/profiles: membership/exclusions, virtual/root projects, default selection, duplicates, invalid members, active ordering, and disabled profiles.
  • Editing: targets, comments, unrelated keys, lower-format promotion, composite values, inheritance after removal, overridden writes, invalid edits, and concurrent writers.
  • Migration: classic field mappings, explicit legacy defaults, relative paths, supported extra fields, unmapped-field failures, existing-target conflicts, idempotence, source preservation, dry-run with no writes, concurrent source/target changes, and equivalence with the classic source excluded. Cover standalone and member projects.
  • Workspace adoption: bounded/excluded/nested inventory, deterministic membership, already migrated projects, root identity, aggregate verification, batch blockers, staged commits, rollback/recovery, and repeat no-ops.
  • Hosts: identical results from in-memory/filesystem adapters, use without CLI dependencies, Microsoft configuration projection, and trimming/AOT.
  • CLI: human/JSON results, stderr diagnostics, exit codes, and end-to-end get/set/unset roundtrips.

Feature status tracking

FeatureStatusNotes
Configuration project and cache modelsImplemented baselineCache-only F# records and merge functions exist
TOML resolver and legacy discoveryImplemented baselineGit-first discovery and user/CI overlays retained for existing callers
Multi-format documents and normalizationImplemented and landedCampaign 1, PR #461; parent landing/cleanup receipt recorded
Shared discovery and project selectionImplemented and landedCampaign 2, PR #467 at 23d274d4c87b367fdc79bd21843a9fdf5d74b13d; parent landing and cleanup receipt recorded
Shared resolution, profiles, validation, provenanceImplemented and landedCampaign 1 additive APIs; prior baseline core 95 and reader/BDD 66; campaign 2 requalification tracked separately
Microsoft configuration integrationImplemented and landedCampaign 3, PR #471; opt-in resolved provider, generated/explicit binding and managed/trimmed/native qualification retained
Shared query and edit servicesImplemented and landedCampaigns 3 and 4, PR #475; shared plans, TOML editor, transactions and parent cleanup confirmed
Classic project migrationImplemented and landedCampaign 5, #476; pure planner, independent verification, guarded IO, project migrate and managed/trimmed/native qualification
Workspace migrationIn ProgressBounded inventory, coordinated project plans, membership, and recovery
Config CLI commandsImplemented and landedCampaign 3 get/list/show/path/explain are landed. Campaign 4 set/unset use shared services; all 360 tooling tests pass, including 33 editing subprocess scenarios. Published-host qualification and parent landing/cleanup are recorded in the prerequisite receipts
Documentation foundationUpdated for inspection and editingShared API and Microsoft consumer examples, read CLI schema/options, walkthrough and troubleshooting; editing API, preview/recovery and CLI examples are implemented; classic project migration reference and walkthrough implemented; workspace migration reference, API and recovery docs are implemented in campaign 6
Compatibility and end-to-end verificationIn ProgressCampaign 1 landed baseline tooling 245, reader 66, core 95 and warning-qualified runtime evidence; current campaign results recorded separately

Implementation notes

Configuration inspection campaign, 2026-11-03

  • Issue #468 and the delivery journal track campaign 3.
  • Keep existing DocumentResolution, Resolver.resolve, ProjectSystem.Load and cache-only APIs compatible. Add detailed resolution with canonical and applied layers for pure queries. Scope filtering precedes query merging; effective queries preserve source ownership. Shared redaction handles nested sensitive values and opaque references before presentation.
  • Morphir.Configuration.Microsoft projects a single resolved document. It rejects case/separator/empty-segment collisions, preserves canonical metadata and masks stale descendants from earlier related host providers. Concrete source-generated binding preserves replaced and empty arrays. Its managed, trimmed and native hosts each pass 43 checks with pinned dependency warnings retained. No warning-free dependency claim is made.
  • Register CLI inspection through one scenario/provider. Discovery is deferred until validated command invocation. Help disables unrelated host defaults that would read appsettings. Normal command execution retains host defaults. Configuration reads do not imply adoption by every existing IR command.
  • JSON nonfinite values use tagged nodes with an external JSON Pointer type discriminator, preserving ordinary tag-shaped user objects. Finite values retain normal JSON types. The output schema and published serializer host qualify this representation.

Discovery landing receipt, 2026-11-03

  • PR #467 landed at 23d274d4c87b367fdc79bd21843a9fdf5d74b13d after PR #461.
  • The parent completion receipt records its landing and owned worktree/branch/temporary-artifact cleanup. Baseline core 124, adapters/BDD 166, tooling 245 and all eight host checks are prerequisite evidence, not a substitute for campaign 3’s validation.

Project/workspace discovery campaign, 2026-11-03

  • Track campaign 2 in issue #466. The delivery journal records red/green checkpoints, validation, base/head identities and outstanding parent gates. Local qualification passes core 124, reader/BDD 153 and tooling 245; authored source coverage is 94.89% core and 96.45% adapters. The implementation change is archived at openspec/changes/archive/2026-11-03-add-project-workspace-discovery/, with the synchronized morphir-project-system main spec. These are local results; PR CI and review remain parent-owned.
  • Keep snapshot/request discovery and project selection pure in Morphir.Configuration. File parsing, native access, mounted trees and machine/environment capture belong to reusable .NET adapters in Morphir.Configuration.IO; no CLI dependency is introduced.
  • Preserve WorkspaceDiscovery, ManagementScenario, and artifact callers as Git-first compatibility entry points. New consumers choose document-aware ProjectSystem explicitly. Discover returns inventory and selection; Load resolves a root or one selected project independently.
  • Keep inventory separate from selection, and owning workspace context separate from root-to-member settings. Invalid literal members remain identifiable. Members cannot relocate the workspace output root.
  • Apply the same format groups to primaries, platform/global alternatives, eligible personal overrides and active profiles. Isolation excludes machine and personal sources before reading them, while retaining declared primaries, explicitly selected configuration, active profiles and supplied values. Machine inputs require explicit locations and a separately authorized MachineTree.

Shared configuration landing receipt, 2026-11-03

  • PR #461 landed at 3d5b41c0f839c7b2648403ad6aba5c5644f7dfb5, with verified squash tree 66609771da2eb88ac9e3cab3c21b6e8d201a6748.
  • The parent completion receipt records its campaign worktree, local/remote branches and 33 reproducible temporary artifacts cleaned. Campaign 2 starts from this main result.
  • Earlier core 95, readers 66, tooling 245, SDK conformance 27, full solution build and trimmed/native smoke results are prerequisite evidence. They do not substitute for validation of campaign 2’s final tree.

Shared configuration implementation, 2026-11-02

  • Track campaign 1 in issue #458.
  • Add typed document APIs beside cache-only records. Parsing and explicit file reading live in Morphir.Configuration.IO; core normalization, profile planning, source resolution and schema validation remain pure.
  • Accept Moon-Lander 0.8.0 Default YAML behavior by explicit user decision, including nulls, anchors/aliases, empty streams and custom scalar tags. Record the intentional difference from core’s stricter YAML profile. JSON/JSON5 duplicate rejection is an approved stricter difference from Moon-Lander.
  • Pin core/MoonBit/framework revisions and fixture hashes. Qualify JSON5 with the official grammar corpus, not comments-only System.Text.Json options.
  • Preserve raw nulls for control typing and raw legacy fields for loss accounting. Ordinary null overlays preserve earlier values; completed loads omit null object fields. Classic checkpoint defaults precede machine settings.
  • Keep commits at coherent task boundaries, with red checkpoints identified. See delivery evidence for exact validation commands/checkpoints. Parent coordination owns review, landing and cleanup; campaign 1 is now complete per the parent landing receipt above. Campaign 2 has its own review, landing and cleanup gates.

Foundation decisions, 2026-11-02

  • Adopt MoonBit’s layered policy instead of the older single-serialization rule.
  • Share configuration/project behavior with other hosts; keep it out of the CLI.
  • Integrate with Microsoft configuration while preserving a typed canonical model.
  • Expose Git-style inspection/editing with scopes, provenance, and typed values.
  • Keep TOML as the written format and explain when another layer reappears or wins.
  • Include explicit classic morphir.json migration with source retention, dry-run, and semantic equivalence checks; loading legacy files alone does not satisfy migration support.
  • Own migration under project/workspace commands, with no config migrate alias. Workspace migration reuses the single-project planner.
  • Deliver six campaigns through merged PRs to main, synchronized/archived specs, and verified campaign-owned worktree/branch cleanup.
  • These foundation notes describe the original planning revision. Later implementation campaigns add APIs without changing the cache-only compatibility entry points.

Open implementation questions

  • Campaign 1 boundaries are Morphir.Configuration and Morphir.Configuration.IO; discovery now uses DiscoveryPolicy, IProjectTree, and ProjectSystem. Editing now uses shared immutable plans, ConfigurationEditService and preconditioned transactions; Microsoft integration landed in campaign 3.
  • Existing cache-only APIs retain their legacy behavior through additive compatibility entry points. Document how consumers explicitly opt into the new discovery and profile model before moving them.
  • Inspection signatures and generated JSON schema are documented in the shared API and CLI references. The editing result schema is published and checked against subprocess reports. The project migration result schema is implemented in campaign 5; workspace migration schema and shared outcomes are implemented in campaign 6.
  • Campaign 1 pins parser/merge/profile fixtures and records coverage/native results. Pinned dependency linker/AOT warnings remain a qualification limitation; strict warning-free trimming is not claimed.

Configuration editing campaign, 2026-11-03

Locally qualified and archived under #473, after merged PR #471. Shared plans, presentation-preserving TOML editing, preconditioned transactions and thin CLI adapters are tracked in the delivery journal. Migration campaigns remain separate.

Classic project migration campaign, 2026-11-03

Campaign 5 tracks #476. The pure planner preserves raw source loss accounting. Independent verification removes converted source and runtime overlays, retaining declared base workspace context. The IO coordinator reuses exact-buffer observations, the TOML editor and transaction boundary. Read-only hosts receive a redacted Presentation. The CLI remains a separate thin project provider. Qualification and parent gates are recorded in the delivery journal.

Workspace migration campaign, 2026-11-03

Campaign 6 tracks #479. The delivery journal records ownership, prerequisite ancestry, task progress and qualification. PR #477 landed at e3b9a053120a49e94c70c30cd4f62ef783672c64; its receipt confirms campaign 5 cleanup. Implementation is locally qualified (all nine tasks), with the OpenSpec change synchronized and archived at openspec/changes/archive/2026-11-03-add-workspace-migration/. Parent owns campaign 6 review, CI watch, landing and cleanup; the campaign remains incomplete.

16.1.6.6 - Vulnerability Resolver Skill Requirements

Product requirements for the Vulnerability Resolver skill - automated CVE detection, resolution, and suppression

Vulnerability Resolver Skill Requirements

Executive Summary

The Vulnerability Resolver skill provides automated assistance for managing security vulnerabilities detected by OWASP Dependency-Check. It enables developers to efficiently triage, fix, or suppress CVEs while maintaining a documented audit trail of security decisions.

Background

Context

FINOS active projects require CVE scanning alongside Dependabot. morphir-dotnet implemented OWASP Dependency-Check scanning in PR #273, which runs:

  • On push/PR to main
  • Weekly on Monday at 3:00 UTC
  • Fails builds on CVSS score >= 7

PR #276 addressed initial vulnerabilities, identifying that some reported CVEs were false positives due to binary scanning misidentification of package versions or confusion with similarly-named packages.

Problem Statement

When dependency scanning detects vulnerabilities:

  1. Developers must manually research each CVE to determine if it’s genuine or a false positive
  2. There’s no standardized process for documenting suppression decisions
  3. Suppression files must be manually created following OWASP Dependency-Check XML schema
  4. No easy way to trigger scans on specific branches during development
  5. No guided workflow for fix vs. suppress decisions

Success Criteria

  1. Automation: Reduce manual effort for vulnerability resolution by 70%
  2. Documentation: 100% of suppressions have documented rationale
  3. Auditability: Clear audit trail for all security decisions
  4. Developer Experience: Interactive prompts guide users through resolution
  5. CI Integration: Ability to trigger scans on any branch

Functional Requirements

FR-1: Scan Triggering

FR-1.1: Trigger dependency-check workflow on any branch

# Example invocation
@skill vulnerability-resolver
Scan branch feature/new-dependency for vulnerabilities

FR-1.2: Support manual workflow dispatch with parameters:

  • Branch/ref to scan
  • Fail threshold (CVSS score, default 7)
  • Output format (HTML, JSON, XML)
  • Suppression file path

FR-1.3: Report scan status and provide link to workflow run

FR-2: Vulnerability Analysis

FR-2.1: Parse dependency-check reports (HTML, JSON, XML formats)

FR-2.2: For each vulnerability, extract:

  • CVE identifier
  • CVSS score and severity
  • Affected package/file
  • Package identifier (purl, CPE)
  • Description and references
  • Whether it’s a transitive dependency

FR-2.3: Categorize vulnerabilities by:

  • Severity (Critical, High, Medium, Low)
  • Fix availability (update available, no fix, N/A)
  • False positive likelihood (based on patterns)

FR-3: Interactive Resolution

FR-3.1: Present vulnerabilities with resolution options:

CVE-2022-4742 (CVSS 9.8) in JsonPointer.Net@6.0.0

Options:
1. Fix: Update to version 6.0.1 (recommended)
2. Suppress: Mark as false positive with reason
3. Skip: Handle later
4. Research: Open CVE details in browser

FR-3.2: For each resolution choice:

  • Fix: Generate package update commands, verify fix in scan
  • Suppress: Create/update suppression XML with documented rationale
  • Skip: Track for follow-up, don’t block

FR-3.3: Detect false positive patterns:

  • Version misidentification in binary scanning
  • Package name confusion (e.g., Cecil vs Mono.Cecil)
  • Already-fixed transitive dependencies
  • Suggest suppression when patterns match

FR-4: Suppression Management

FR-4.1: Create and manage suppression file (dependency-check-suppressions.xml)

FR-4.2: Suppression file structure following OWASP schema:

<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
  <suppress until="2025-12-31">
    <notes><![CDATA[
      False positive: CVE-2023-4914 targets Cecil static site generator,
      not Mono.Cecil library. Verified package source.
      Suppressed by: @username
      Date: 2024-01-15
      Review: Quarterly
    ]]></notes>
    <cve>CVE-2023-4914</cve>
  </suppress>
</suppressions>

FR-4.3: Suppression methods supported:

  • By CVE identifier
  • By package URL (purl)
  • By CPE
  • By file path (regex)
  • By SHA1 hash

FR-4.4: Required suppression metadata:

  • Reason for suppression
  • Who approved the suppression
  • Date of suppression
  • Review date (recommended: quarterly)
  • Optional expiration date (until attribute)

FR-4.5: Integrate suppression file with workflow:

args: >
  --failOnCVSS 7
  --enableRetired
  --suppression ./dependency-check-suppressions.xml

FR-5: Fix Automation

FR-5.1: Generate fix commands for different package managers:

# NuGet (Directory.Packages.props)
# Update JsonPointer.Net from 6.0.0 to 6.0.1

# In Directory.Packages.props:
<PackageVersion Include="JsonPointer.Net" Version="6.0.1" />

FR-5.2: Verify fix effectiveness:

  • Check if new version resolves CVE
  • Warn if update introduces breaking changes
  • Validate update doesn’t introduce new CVEs

FR-5.3: Handle transitive dependencies:

  • Identify which direct dependency pulls the vulnerable package
  • Suggest upgrade path
  • Note when fix requires waiting for upstream update

FR-6: Reporting and Documentation

FR-6.1: Generate resolution summary:

## Vulnerability Resolution Summary

**Scan Date**: 2024-01-15
**Branch**: main
**Total Vulnerabilities**: 4

### Fixed (1)
- CVE-2022-4742 in JsonPointer.Net: Updated 6.0.0 → 6.0.1

### Suppressed (3)
- CVE-2023-36415 in Azure.Identity: Already fixed in 1.17.1 (transitive)
- CVE-2023-4914 in Mono.Cecil.Mdb: False positive (different package)
- CVE-2012-2055 in Octokit: Not applicable to this library

### Pending (0)
None

FR-6.2: Maintain resolution history for audit purposes

FR-6.3: Generate PR description for vulnerability fixes

Non-Functional Requirements

NFR-1: Security

  • Never expose actual vulnerability details in logs
  • Suppression decisions must be committed to version control
  • Support for security team review workflow

NFR-2: Performance

  • Skill invocation < 5 seconds for analysis
  • Report parsing < 10 seconds for typical reports
  • No impact on regular CI pipeline speed

NFR-3: Maintainability

  • Follow existing skill template patterns
  • Reusable scripts for automation
  • Clear documentation for manual fallback

NFR-4: Auditability

  • All suppressions traceable to commits
  • Suppression history preserved
  • Quarterly review reminders

Technical Design

Workflow Modifications

Update .github/workflows/cve-scanning.yml to support:

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 3 * * 1'
  workflow_dispatch:
    inputs:
      branch:
        description: 'Branch to scan'
        required: false
        default: 'main'
      fail-cvss:
        description: 'Fail on CVSS score >= N'
        required: false
        default: '7'
      suppression-file:
        description: 'Path to suppression file'
        required: false
        default: './dependency-check-suppressions.xml'

Skill Files Structure

.claude/skills/vulnerability-resolver/
├── SKILL.md              # Main skill definition
├── README.md             # Quick reference
├── MAINTENANCE.md        # Maintenance guide
├── scripts/
│   ├── scan-branch.fsx          # Trigger scan on branch
│   ├── parse-report.fsx         # Parse DC reports
│   ├── create-suppression.fsx   # Generate suppression XML
│   └── verify-fixes.fsx         # Verify CVE fixes
└── templates/
    ├── suppression-entry.xml    # Suppression template
    └── resolution-summary.md    # Summary template

Integration Points

QA Tester Skill: Coordinate for regression testing after dependency updates Release Manager Skill: Ensure no unresolved vulnerabilities before release AOT Guru Skill: Verify dependency updates don’t break AOT compatibility

User Stories

US-1: Developer Fixes Vulnerability

As a developer, when the dependency check fails, I want to quickly identify which vulnerabilities are genuine and how to fix them so I can unblock my PR.

US-2: Security Review for False Positive

As a developer, when I identify a false positive, I want to suppress it with proper documentation so future scans don’t flag the same issue.

US-3: Pre-merge Vulnerability Check

As a developer, I want to check my branch for vulnerabilities before creating a PR so I can address issues proactively.

US-4: Quarterly Security Review

As a maintainer, I want to review all active suppressions quarterly to ensure they’re still valid and no fixes have become available.

US-5: Audit Trail

As a security auditor, I want to see a complete history of vulnerability decisions so I can verify the project follows security best practices.

Implementation Phases

Phase 1: Core Infrastructure (MVP)

  • Update workflow for manual dispatch
  • Create suppression file with initial false positives
  • Basic skill definition with manual resolution workflow
  • Create GitHub issue for tracking

Phase 2: Automation

  • Report parsing scripts
  • Suppression generation scripts
  • Fix verification scripts
  • Interactive resolution prompts

Phase 3: Integration

  • Integration with other skills
  • Quarterly review automation
  • Resolution history tracking
  • PR description generation

Appendix

A. Known False Positive Patterns

PatternExampleDetection
Version misidentificationAzure.Identity@1.1700.125.56903Assembly version != package version
Package name confusionCecil vs Mono.CecilCheck actual package source
Stale CVECVE-2012-2055 for Octokit@14.0.0CVE date significantly older than package

B. OWASP Dependency-Check References

  • #272: Add code scanning tools to the repo
  • #273: Add CVE scanning workflow for vulnerability detection
  • #275: Fix reported dependency vulnerabilities
  • #276: Fix CVE-2022-4742 by updating JsonPointer.Net

Document Version: 1.0.0 Status: Draft Author: Claude Code Date: 2024-12-19

16.1.6.7 -

Configuration editing delivery journal

Tracking issue: #473. Change: add-config-editing. Implementation is locally qualified and archived; review, CI, landing and cleanup remain parent-owned.

Ownership and prerequisite

  • Owned checkout: /Users/damian/.t3/worktrees/morphir-dotnet/t3-62fedf74.
  • Owned branch: t3/add-config-editing. Initial checkout was clean.
  • Base: 422ff4efa3139868bc73f79d1cdf4df529e2b34f.
  • Prerequisite PR #471 is merged at that base. Validated head a5efd7dc9dec893025c7da05cc4d7c8017efc00a and base have identical tree 43f9469cce6a64009cda037ef856f9663ee67a97.
  • Authoritative completion receipt confirms prerequisite checks, review fixes, archive and owned cleanup.
  • Campaign temporary root: /private/tmp/morphir-config-editing, including physical tmp, editor, transaction and validation evidence. Parent owns eventual cleanup; unresolved recovery artifacts must be preserved.
  • Parent owns independent review, CI monitoring, merge and checkout/branch/temp cleanup. This implementation worker will not watch, merge, force-push or delete branches.

Work allocation

The implementation worker owns shared F# models/planner, IO orchestration, CLI, documentation and delivery. Two helpers own disjoint TOML editor and transaction implementation/test files. Only one heavy .NET job runs at a time, with DOTNET_PROCESSOR_COUNT=2, -m:1 -p:BuildInParallel=false, bounded executable test workers and sequenced Expecto. All processes use physical TMPDIR, TEMP, TMP beneath the campaign root.

Validation

Tests are recorded red before production behavior, then green after implementation. No baseline count is claimed as a result.

Shared plans boundary

  • core-red-01.log proves missing plan API; core-green-01.log passes the initial immutable string plan.
  • core-red-02.log/core-green-02.log qualify complete typed values. Selection, unset/inheritance, hidden invalid values, numeric/path-shape guards and virtual-root personal ownership have subsequent red/green checkpoints 03–07.
  • core-full-build.log and core-full.log pass all 147 core Expecto tests after formatting. The 11 new tests cover typed values, scope selection, quoted paths, immutable observations, inheritance, redaction and numeric semantics.
  • io-plan-red.log proves the reusable IO planning API is absent before implementation. io-plan-green.log proves an in-memory preview invokes no writer operations.
  • Exact-buffer evidence, independent explicit-path authorization and canonical candidate topology resolution have io-plan-red-02.log/io-plan-green-02.log evidence. Isolated-read parity has io-plan-red-03.log/io-plan-green-03.log. The latter passes 10 integrated IO cases.
  • Actual CLI subprocess BDD is red before registration. cli-red-02-current.log, after rebuilding the actual CLI, fails 11 of 23 cases for unimplemented flags and raw-reference presentation. cli-green-03.log passes all 23 after implementation; builds refresh src/Morphir/bin before subprocess tests.

The global CLI fixture uses a separate captured home beneath its test root, preventing a global fallback from also becoming a conflicting project primary. On this macOS host /etc is a link. Native system preview obeys the existing no-follow policy and returns typed exit 2 without a write. Pure system selection and explicitly authorized physical targets remain available. No filesystem-link policy was widened to pass a test.

TOML editor boundary

The editor qualification records red/green checkpoints 01–12 and the final 30/30 focused tests. Source and test builds have zero warnings/errors. The editor preserves token spans, comments, BOM, line endings, quoted/dotted key spelling and following-header presentation. Complete inline/table/array edits are verified against the canonical reader, including temporal/nonfinite values, NaN equality, signed zero, opaque references and the pinned TOML corpus. No parser dependency or accepted TOML version changed.

The extended IO host passes 37 new managed checks plus the existing 228 JSON5 checks, 26 Morphir fixture documents and discovery/machine-routing checks. smoke/managed-build.log has zero warnings/errors; smoke/managed-run.log records editor 9, shared editing 15 and transaction 13. Trimmed/native publication is still pending at this boundary.

IO and transaction boundary

The transaction qualification records 32 focused passing cases and paired failing/passing receipts for staging, replacement, rollback, cleanup, ownership, same-process and actual cross-process coordination. In-memory application and immutable candidate copies are verified. Native coordination is machine-local, not distributed.

Integrated io-workspace-red.log proves both explicit workspace and root-default writes originally reported the wrong effective view when a selected member still won. io-workspace-green.log passes both after target selection was separated from effective resolution. io-edge-red.log identifies unstable planning evidence as incorrectly categorized I/O; io-edge-green.log proves it now reports a conflict. Tests also retain selected member/personal ownership under isolation, refuse explicit alternate-primary ambiguity, exercise complete inline dotted subtrees, reject lossy Unicode, retain BOM encodings and detect blocked parent paths without modifying them.

After these cases, core-final.log passes 148/148 Expecto tests and io-edge-green.log passes the full 266/266 IO/TUnit/Reqnroll tests. Builds report zero warnings/errors. Current-source coverage in coverage-final-summary.json unions core-final-coverage/*/coverage.cobertura.xml and io-edge-coverage.cobertura.xml: core 3757/3913, 96.01%; IO 1381/1425, 96.91%. Both exceed the prerequisite’s authored-source measurements. The executable Expecto runner is the correctness gate; the adapter-based invocation supplies coverage only.

The actual generated report sources also pass a 71-check managed serializer host with reflection serialization disabled, including editing before/after, raw-reference redaction, overridden values, recovery ownership and depth 127. serializer-managed-build.log reports zero warnings/errors.

CLI boundary

validation/Morphir.Tooling.Tests.log passes all 360 TUnit/Reqnroll cases, including 33 editing subprocess scenarios and 18 no-discovery/host-binding checks. Every editing subprocess report is checked against docs/spec/schemas/config-edit-result.schema.json with a fresh schema registry. Persisted values prove default strings, booleans, Int64, floats including large finite literals, complete arrays/objects and literal dotted keys. Unset reveals classic JSON with its origin. Before/after opaque references are redacted, previews preserve bytes, successful overridden writes return 0 and missing unset returns 1.

core-number-red.log reproduces rejection of a large literal despite explicit floating input; core-number-green.log passes after accepting finite explicit numbers while retaining Int64/composite integer limits. This changes edit input typing only; canonical readers and accepted TOML/YAML/JSON/JSON5 profiles remain unchanged.

The first broad local driver stopped after five passing suites on Bash 3.2’s empty-array handling. That driver was repaired to use positional arguments and infer Expecto from project metadata; no product change was needed. Its evidence is preserved in validation-initial/. The current broad run uses each configured executable runner, one TUnit worker, sequenced Expecto, bounded nested builds and a process-local path to the already installed PowerShell 7.5.3. No global tool setting changed.

Final local qualification

The source checkpoint is cb6ff29. All 17 configured runners pass; validation/summary.json records these exact counts.

RunnerPassed
Morphir.CodeGeneration.FSharp.Tests333
Morphir.Configuration.IO.Tests266
Morphir.Configuration.Inspection.Smoke71
Morphir.Configuration.Microsoft.Tests22
Morphir.Configuration.Tests149
Morphir.Core.Tests237
Morphir.IR.Pipeline.Plugins.Tests141
Morphir.IR.Pipeline.Tests299
Morphir.Internal.CodeGeneration.Tests25
Morphir.Ion.Tests463
Morphir.Models.Tests791
Morphir.Myriad.Integration.Tests12
Morphir.Myriad.Tests4
Morphir.SDK.CSharp.Tests20
Morphir.SDK.Conformance.Tests27
Morphir.SDK.Tests403
Morphir.Tooling.Tests360

The solution build passes with 20 existing package/generated-code warnings and no errors. C# formatter verification and Fantomas verification pass. The new managed configuration and serializer builds have no warnings/errors. delivery-coverage.json records core 3766/3922, 96.02%, and IO 1381/1425, 96.91% on the final source.

Both non-CLI hosts pass managed, trimmed and native osx-arm64 qualification. The IO host passes 37 new checks, all 228 JSON5 grammar cases, 26 pinned Morphir documents and its existing host/discovery/machine-root checks. The linked-source CLI serializer host passes 71 checks in each mode, with reflection serialization disabled. publish-summary.json records actual receipts and warning counts. IO trimmed/native retain 44/47 pinned dependency warning lines; serializer trimmed/native retain 42/47. ILLinkTreatWarningsAsErrors=false is explicit on each publish, no diagnostic is suppressed, and no warning-free dependency claim is made. No authored-source linker/AOT diagnostic occurs. Exact prepublish lockfile bytes were restored through the captured SHA-256 manifest.

Reproduction

Run one heavy job at a time from the repository root. Use physical paths on macOS:

export DOTNET_PROCESSOR_COUNT=2 BuildInParallel=false
export TMPDIR=/private/tmp/morphir-config-editing/tmp
export TEMP="$TMPDIR" TMP="$TMPDIR"
export PATH="/Users/damian/.local/share/mise/installs/powershell/7.5.3:$PATH"
dotnet build Morphir.slnx -m:1 -p:BuildInParallel=false
# For each runner in the table, use its compiled executable:
dotnet tests/Morphir.Configuration.Tests/bin/Debug/net10.0/Morphir.Configuration.Tests.dll --sequenced
dotnet tests/Morphir.Configuration.IO.Tests/bin/Debug/net10.0/Morphir.Configuration.IO.Tests.dll --maximum-parallel-tests 1
# Other Expecto projects use --sequenced; other TUnit projects use one worker.
dotnet format Morphir.slnx --no-restore --verify-no-changes --verbosity quiet
dotnet fantomas --check src/Morphir.Configuration/Editing.fs tests/Morphir.Configuration.Tests/EditingTests.fs

run-validation.sh under the campaign evidence root contains the complete 17-runner sequence and bounded process environment. It reads each test project’s framework metadata; the serializer smoke takes no runner flags. Nested SDK packs/builds remain serial, using the installed process-local PowerShell. Runtime source is unchanged by the later archive and documentation commits.

Coverage is measured after correctness gates:

dotnet tests/Morphir.Configuration.IO.Tests/bin/Debug/net10.0/Morphir.Configuration.IO.Tests.dll --maximum-parallel-tests 1 --coverage --coverage-output-format cobertura --coverage-output "$TMPDIR/io.cobertura.xml"
dotnet test tests/Morphir.Configuration.Tests/Morphir.Configuration.Tests.fsproj --no-build --no-restore -m:1 -p:BuildInParallel=false --collect:"XPlat Code Coverage" --results-directory "$TMPDIR/core-coverage"
/usr/bin/python3 scripts/configuration/summarize-coverage.py "$TMPDIR"/core-coverage/*/coverage.cobertura.xml "$TMPDIR/io.cobertura.xml"

Each smoke README gives exact managed/trimmed/native commands. The published modes use -m:1 -p:BuildInParallel=false -p:PublishTrimmed=true -p:PublishAot=<false|true> -p:TrimmerSingleWarn=false -p:ILLinkTreatWarningsAsErrors=false -p:JsonSerializerIsReflectionEnabledByDefault=false. The IO executable receives --fixtures <repo>/tests/Fixtures/Configuration. run-publish.sh and the lockfile capture/restore manifest retain the exact successful sequence.

Main integration and archive

Main advanced through the documentation-only SDK archive PR #472, landing at 618fbf7b6c5b542122450bf61f4f794e590fe4f5. It was merged normally into the owned branch. Runtime sources and configuration/tooling tests are byte-identical to cb6ff29. Independent SDK main specs and archives match main exactly. Fresh SDK conformance passes 27/27 after integrating its readiness metadata. No SDK archive was authored or altered by this campaign.

Only add-config-editing was synchronized and archived at openspec/changes/archive/2026-11-03-add-config-editing/. Its main capability is openspec/specs/configuration-editing/spec.md. All eight tasks are complete. The nine requirement bodies, 13 scenarios and Purpose match the approved delta exactly. The archived design/proposal/task links are checked; the classic migration prerequisite link is repaired. Independent migration work remains unimplemented.

Strict OpenSpec validation passes 22/22 active/spec items before and after archiving. The html-design snapshot check and both extension tests pass. The generated-output publication main spec and implementation remain unchanged. The archive receipt and task-boundary instruction snapshots are retained under the campaign evidence root.

Handoff limits and ownership

Local qualification does not claim current-head CI, independent review, landing or campaign completion. Parent owns those gates, including the explicitly authorized workstream admin bypass and subsequent checkout/local-branch/remote-branch/temp cleanup. The implementation worker does not watch, merge, force-push, delete branches or publish releases.

Native writes coordinate cooperating processes on one machine; shared stores needing distributed coordination must supply it through their host. Per-file atomic installation does not provide multi-file group/crash atomicity. Native no-follow policy can reject the conventional macOS /etc alias; an authorized physical TOML target avoids that alias. Windows paths and separate drive/UNC hosts are covered by the shared host fixtures; the local native publication target is osx-arm64.

All owned external build/test/publish evidence is under /private/tmp/morphir-config-editing/, including tmp, editor, transaction, smoke, publish, publish-locks, validation, validation-initial, coverage files and instruction/archive receipts. Generated bin, obj and test outputs inside the retained owned checkout also belong to this campaign. Shared tool/package caches are not campaign cleanup targets. No unresolved native recovery artifact was found after the successful qualification runs. Preserve any recovery data discovered during later review instead of deleting it to manufacture a clean result.

Parent Windows CI fixture correction

Windows run 41671846502 on 9b683b9 passed all 360 tooling tests but failed 21 IO transaction cases. The native observations returned canonical ‘/’ paths, while the test helper supplied native backslash paths as change destinations. The existing plan-ownership guard correctly rejected that mismatch before any write, producing typed I/O outcomes and preventing fault/staging callbacks.

Transaction fixtures now construct canonical paths with ProjectPath.join, and normalize directory-enumeration results before path assertions. The public example uses observed.Path for the corresponding change, making its ownership contract explicit on Windows. No production guard or native locking behavior was weakened. The unchanged native CLI execution had already passed Windows CI. The corrected full IO runner passes 266/266 locally; test compilation, formatter verification and whitespace checks pass. Evidence is owned under /private/tmp/morphir-config-editing/parent-review/, including the downloaded Windows failure log and windows-path-io-full.log. Ubuntu and macOS were cancelled by matrix fail-fast after Windows failed; fresh CI remains required.

The independent TOML/transaction review completed without actionable findings. Its existing-DLL probes passed 1,438 editor operations, 10,000 sampled finite double roundtrips and 8 native transaction cases. Its disposable evidence is /private/tmp/morphir-config-editing-review-writes; no recovery data was retained.

Parent shared-planning review fixes

The independent planning/CLI review reproduced two contract defects on 9b683b9. A user edit filtered personal candidates by the preferred primary’s directory, missing an existing personal TOML or lower-format stem in another eligible layout. It could reject a valid set through a newly introduced ambiguity or return not found for an existing unset entry. Target selection now filters by the complete set of eligible personal paths from the actual owner, preserving format-group ambiguity checks and keeping another owner’s personal sources separate.

An explicit personal file in another declared member was treated as an arbitrary Selected source because only the default member’s inventory was considered. Its workspace output could then relocate the reported owning workspace. The reusable IO service now identifies an explicit member-personal owner from every discovered member’s eligible layouts, and uses that owner for both initial and candidate resolution. The target keeps its UserOverride role and member scope, including under isolation or an otherwise ambiguous virtual-root selection. Workspace-root personal sources and arbitrary explicit files retain their existing handling.

Eleven public IO regressions failed before these fixes, in ownership-red.log. They cover standalone/member set and unset, lower-format promotion, personal TOML ambiguity, all three personal layouts, isolated selection and a virtual root. The final 30-case editing-service group passes, ownership-io-focused.log. One intermediate test expected the member primary to win after unset; its fixture also supplied a higher workspace-personal value, so that assertion was corrected to the unchanged layering contract. No production layering rule changed.

Three new subprocess BDD cases failed against the previous actual CLI, in ownership-cli-red.log, then passed after rebuilding the CLI with the shared fixes, ownership-cli-green.log. They assert set/unset on a nonpreferred personal layout and explicit other-member ownership. The IO smoke adds seven shared checks for those same paths. Managed qualification passes editor 9, shared editing 22, transaction 13, plus all 228 JSON5 cases, 26 Morphir fixtures and existing host checks, in ownership-io-managed.log. Shared API and CLI documentation describe the corrected selection/ownership behavior. The main spec’s nine requirements and 13 approved scenarios remain unchanged.

The reviewer probes are owned at /private/tmp/morphir-config-editing-review-planning. No review was posted as a human approval. Parent-owned current-head CI, final qualification, landing and cleanup remain outstanding at this checkpoint.

Parent final review qualification

Runtime fixes are committed at fa7782d; the separate Windows fixture correction is 214eee4. All 17 configured runners pass on the final runtime source, using one TUnit worker, sequenced Expecto and serial nested builds. The refreshed counts are core 149, IO/BDD 277, tooling/BDD 363 and Microsoft 22; the other configured suites retain their baseline counts. All three new subprocess regressions pass. parent-review/final-validation.json records every exit code and links to the corresponding final-<project>.log receipts.

The full solution build succeeds with no errors and 9 existing warnings on this incremental invocation. Changed C# formatter verification and F# Fantomas checks pass. OpenSpec verification passes all 22 active/spec items and both extension tests. Current-source coverage unions the refreshed core collector and IO coverage files, never the pre-edit source-line mappings: core 3775/3932, 96.01%, and IO 1392/1436, 96.94%, in ownership-coverage-summary.json.

Fresh managed, trimmed and native osx-arm64 IO hosts pass all 44 editing checks, 9 editor, 22 shared planning/application and 13 transaction, as well as 228 JSON5 syntax checks, 26 pinned Morphir documents and existing host/discovery/machine checks. New mixed-layout personal and explicit other-member owner paths execute in each published mode. Receipts are ownership-io-managed.log, io-trimmed-result.json and io-native-result.json; published stderr is empty. Pinned dependency linker diagnostics remain visible under explicit ILLinkTreatWarningsAsErrors=false, with no authored-source diagnostic suppression. Publish-only lockfile changes were restored byte-for-byte from the captured SHA-256 manifest; the checkout is clean apart from this qualification record.

The serializer host still passes 71 managed checks in the full run. Its prior 71-check trimmed/native receipts remain applicable: neither report/DTO/context source nor dependency versions, target framework or publish options changed. The changed F# selection helper and IO planning service are outside that host’s call graph; its shared Editing.plan calls and serialization/redaction behavior remain unchanged. Newly changed selection paths have the fresh IO-host native qualification above.

Main was refreshed and remains validated base 618fbf7b6c5b542122450bf61f4f794e590fe4f5. Both independent reviews have completed; all reported material findings are addressed. Fresh current-head CI remains required before the authorized landing and owned cleanup. All added parent evidence remains under /private/tmp/morphir-config-editing/parent-review/.

16.1.6.8 -

TOML editor qualification

Configuration editing uses the existing pinned CsToml 1.8.6 reader to accept source documents and verify rendered candidates. It adds no parser package and makes no change to the reader’s accepted TOML profile. The existing TOML 1.0 compatibility contract remains in force. The installed package README states that 1.0 is the default and 1.1 is optional through CsTomlSerializerOptions.Spec; the editor does not enable that option. Its NuGet metadata identifies the MIT license and upstream commit 6e3b51a9ac9b4bf8f51c9175ffa6f74cd73940ea. Reader conformance fixtures and rejection tests remain authoritative for parsing.

TomlEditor scans source spans for the requested edit. It changes a value token or the necessary named entries and table declarations. It does not serialize the whole document. Before returning a successful edit it reparses the candidate and compares the entire raw typed tree with the intended tree. A lexical mistake therefore returns config.edit.verification and the original text. Parse failures and unrepresentable values likewise return safe codes and the original text. No input values appear in these diagnostics.

The editor is repository-authored code under the repository’s Apache-2.0 license. CsToml continues as the unchanged MIT-licensed package dependency. No upstream source is copied into the editor. It uses explicit discriminated-union matches, source text operations and invariant primitive formatting. It has no dynamic code, reflection serializer or runtime secret resolution. Existing pinned CsToml/FSharp.Core trimming diagnostics remain the dependency qualification limitation; this change does not suppress them.

The transaction adapter owns encoding and atomic file installation. The editor returns text without normalizing unrelated whitespace or line endings. Missing unset results and byte-identical writes do not require installation. Syntax fixtures cover both presentation and complete semantic equality through the canonical reader.

Presentation and semantic checks

The focused tests exercise CRLF and inline comments, exact quoted and literal key spelling, dotted paths, implicit empty parent objects, complete table replacement and removal, nested inline objects, and array-of-tables replacement. Multiline strings containing comment/header characters, arrays with comments, temporal values and unrelated nonfinite numbers remain byte-identical. Candidate comparison treats two NaNs as the same typed number; ordinary F# structural equality alone would reject an unrelated edit in a document containing NaN.

Red and green checkpoints are recorded under /private/tmp/morphir-config-editing/editor/. red-01.log fails because the editor does not yet exist. Subsequent pairs record unset, insertion, missing-unset, complete table changes, inline changes, implicit-parent preservation and the NaN regression. green-08.log passes the first 17 cases. Later qualification expands this suite; the campaign delivery journal records the final counts and host-publishing results.

Unsetting the final leaf retains its parent object. If a parent existed only through dotted entries, the editor may add an explicit empty object to preserve that semantic container. Unsetting the parent itself removes its declarations and descendants. Arrays are complete values. Traversal through an array, scalar or opaque secret reference fails before rendering. Setting a complete object replaces that object’s authored entries; it does not merge the object with its previous value.

New entries are inserted after the last complete statement line in their containing table. Leading comments and indentation on the next unrelated header stay together. Existing key spelling and quote style remain intact when changing only a value. Newly rendered values use invariant types, including an explicit float marker for integral doubles and the sign of negative zero.

Final focused qualification passes 30/30 cases in editor/final.log, after formatting the owned source and test files. The build reports zero warnings and errors. The additional cases cover canonical opaque references, sign changes between positive and negative zero, typed scalar/composite roundtrips including subnormal/maximal doubles and Unicode controls, null/range rejection, BOM and 127-segment quoted paths, TOML 1.1-only syntax rejection, and all three pinned upstream TOML documents. The HTML result is under editor/results/. These are local managed checks; the campaign journal records the separate trim/native and integrated suite evidence.

Parent integration adds four passing cases for complete inline dotted subtrees, multiline array comments/partial string quotes, control escapes and safe refusal of lossy Unicode. The integrated editor count is 34; the full IO suite has 266 passing tests at this boundary.

16.1.6.9 -

Configuration file transactions

Morphir.Configuration.IO exposes ConfigurationTransaction independently of CLI discovery and presentation. A host captures ConfigurationObservation values for every input used by a preview, including absent candidate files, and passes those observations with immutable ConfigurationFileChange values to ConfigurationTransactionPlan.

var observed = ConfigurationTransaction.Observe(target);
var plan = new ConfigurationTransactionPlan(
    observations: new[] { observed },
    changes: new[] { new ConfigurationFileChange(observed.Path, observed, candidateBytes) });
var result = new ConfigurationTransaction().Apply(plan);

Native observations return canonical / paths on every platform. When starting with a native path, use the returned observation path for the corresponding change, as above.

The editing service supplies the complete observation set. Constructing a transaction directly makes the caller responsible for including its source and target-selection evidence. The target’s expected observation is automatically included even if the caller omits it from the general observation list.

IConfigurationTransactionHost is the explicit host boundary for in-memory or embedded applications. Its methods capture content/inventory identities, coordinate writers, stage bytes, install one destination, and remove owned artifacts. A host’s Install implementation must replace a single file atomically. Multiple installations do not constitute a group-atomic transaction.

Native coordination

The native host serializes cooperating configuration writers with one named OS mutex. This deliberately serializes disjoint edits too. Configuration writes are infrequent, and a single coordinator avoids filesystem case aliases and overlapping directory inventories bypassing separate path locks. The Unix name is Morphir.Configuration.Edit.v1; Windows uses Global\Morphir.Configuration.Edit.v1. Access or platform failures return an I/O outcome; the implementation does not fall back to an unlocked write. An abandoned mutex grants a new lease, after which every planned observation is checked again.

The lease remains held through recovery and artifact cleanup. Native coordination creates no files alongside configuration targets. The OS owns the named synchronization resource and releases its handles when the lease is disposed or its process exits. Custom hosts must provide a coordination protocol shared by all their writers.

Content identities use SHA-256 rather than timestamps. Directory observations capture inventory evidence; missing files and absent alternate primaries have explicit observations. The transaction rechecks every input after staging and immediately before each installation. It excludes only its own artifact paths from directory inventories and accounts for directories it created for the target. A cooperative writer that obtained an old preview conflicts after the current writer releases its lease.

The named OS coordinator is machine-local. A shared network store with writers on multiple machines needs an injected host with shared-store coordination. The native adapter does not claim distributed locking.

The protocol coordinates hosts that use this transaction service. An unrelated program can still change a file after the final observation; portable filesystem rename does not provide content compare-and-swap. Such edits are outside the coordinator. Recovery never overwrites a target whose current bytes differ from both the original and the staged candidate.

Validation and fault-test evidence will be recorded in the campaign delivery journal before handback.

Recovery evidence

Each changed destination receives a stage, an original-byte backup when it existed, and a versioned JSON recovery journal before installation. Names contain a unique transaction identifier and do not use the configuration-primary filenames. The journal records ownership, target, original and candidate identities, and stage/backup paths. It contains no raw configuration values. Unix staging files are created with mode 0600; the backup and staged configuration may contain secrets and must be protected as user configuration.

ConfigurationTransactionPhase hooks inject faults at staging, replacement, recovery, and cleanup boundaries. A failure after replacement attempts rollback while holding the coordinator. The original bytes are restored only when the current target still matches this transaction’s candidate. A missing original is restored by removing the newly installed candidate. Another writer’s bytes are preserved.

The result categories distinguish written, unchanged, conflict, I/O failure before installation, fully rolled-back failure, and recovery required. A recovery-required result identifies every retained artifact by path, kind, and transaction owner. It never represents an unchanged or successful outcome. Keep its journal and backup together, compare the journal’s identities with the current target and backup, then recover under the same writer coordination. The API does not automatically recover or delete a previous transaction’s artifacts.

Each destination is installed atomically, but earlier destinations can be visible before a later installation fails. Recovery can also fail. Callers must not describe multiple file changes as group atomic.

An injected host that fails after creating an artifact must remove it before throwing or throw ConfigurationArtifactFailureException with its owned paths, kinds, and transaction identifier. The service cleans artifacts owned by its transaction if possible. It preserves artifacts with another owner and reports recovery required instead of deleting them. Native staging follows this contract even when writing or flushing a partially created file fails.

Before attempting manual recovery, verify the journal owner and the original backup’s SHA-256 identity. A target that differs from both recorded identities requires an operator decision; do not overwrite it automatically. A host can use a fresh transaction with the current target and backup observations to restore approved bytes. Remove the previous transaction’s artifacts only after recovery is verified and only after confirming their ownership. A normal new edit never prunes older recovery evidence.

Existing destinations use File.Replace; new destinations use a non-overwriting File.Move. Both stages live beside the destination. Unix replacement copies the existing permission bits onto the candidate before replacement. New files retain the restrictive staging mode. Byte-identical candidates bypass staging and installation, preserving the target’s last-write timestamp.

Validation evidence

The focused executable TUnit suite is ConfigurationTransactionTests. Campaign logs live under /private/tmp/morphir-config-editing/transaction/; the campaign delivery journal records final integrated results.

BehaviorFailing evidencePassing evidence
Replacement rollbackred-rollback.loggreen-rollback.log
Changed source or absent alternate primaryred-recheck.loggreen-recheck.log
Linked ancestor rejectionred-links.log, red-target-guard.loggreen-links.log, green-target-guard.log
Directory evidence with owned stagingred-directory.loggreen-directory.log
Overlapping writersred-lock.loggreen-lock.log, cross-process.log
Incomplete recovery journalred-journal.loggreen-journal.log
Cleanup failurered-cleanup.loggreen-cleanup.log
Recovery target and backup integrityred-recovery-guard.loggreen-recovery-guard.log
Partially created stage ownershipred-partial-stage-build.loggreen-partial-stage.log
Proven post-rollback outcomered-fault-matrix.loggreen-fault-matrix.log
Existing Unix permissionsred-mode.loggreen-mode.log
Target evidence ownershipred-plan-guard.loggreen-plan-guard.log
Stage and backup integrity before installationred-stage-integrity.loggreen-stage-integrity.log
Unambiguous inventory identitiesred-inventory.loggreen-inventory.log
Partially created parent ownershipred-parent-ownership.loggreen-parent-ownership.log

qualification.log verifies immutable input snapshots, a wholly in-memory host, byte-identical no-op timestamps, and observable per-file installation followed by multi-file rollback. cross-process.log runs a second executable TUnit process against the same native coordinator. It proves that the child cannot stage while the parent owns the lease and that the stale child conflicts after the parent installs its candidate. Windows-specific namespace and metadata behavior still require Windows CI; local qualification ran on macOS.

Hosts that create target directories can implement CreateParentDirectories(destination, transactionId) to attach the current owner to failures. The default overload preserves existing host implementations; those implementations must clean any partial directory creation themselves before throwing. The native implementation returns canonical / paths and either cleans created directories or reports typed retained ownership.

16.1.6.10 -

Configuration inspection delivery

Campaign 3, issue #468.

Baseline and ownership

Clean T3-prepared checkout /Users/damian/.t3/worktrees/morphir-dotnet/t3-a74ab7cb, branch t3/add-config-inspection, based on current main 23d274d4c87b367fdc79bd21843a9fdf5d74b13d. Fetch and ancestry checks confirm campaign 1 landed at 3d5b41c0f839c7b2648403ad6aba5c5644f7dfb5 and campaign 2 at this base.

Campaign 2 completion receipt records prerequisite landing and owned cleanup. Campaign 1 receipt is recorded separately. Prior evidence does not replace validation of this campaign.

Only inspection is implemented here. Editing and migration remain active plans. Parent owns reviews, check monitoring, merge and subsequent owned cleanup. No watch, merge, bypass, force push, branch/worktree deletion, deployment or release is performed by this implementation agent.

Execution policy

Heavy .NET jobs run serially with DOTNET_PROCESSOR_COUNT=2, MSBuild -m:1 and bounded workers. Filesystem tests use physical TMPDIR, TEMP, TMP under /private/tmp/morphir-config-inspection. Reproducible logs and binaries live there. docs/superpowers/ is locally excluded and never staged.

The existing resolution result and constructor remain compatible. Additive detailed resolution retains canonical layers and applied values for queries without rereading sources. Microsoft projection consumes one resolved document.

Red checkpoints

  • Shared tracer compile failed because resolveDetailed and Inspection did not exist, then its runtime test failed against a minimal get stub: effective global value expected but missing. Logs core-compile-red.log, core-runtime-red.log under the owned temp root. The configured Expecto runner selected Inspection with --filter-test-list Inspection --sequenced.

  • Microsoft adapter compile failed for missing projection API, then executable TUnit/Reqnroll ran 13 cases against no-behavior stubs: 13 failed. Logs microsoft-compile-red.log, microsoft-runtime-red.log.

  • Shared query cases grew incrementally after the tracer: quoted-path compile red then 2 green; recursive secret/container redaction runtime red then 3 green. List/source-report/explain stubs then fail 3 of 7 cases, followed by embedded-host Reqnroll red 3/4. Logs keypath-*, redaction-*, queries-red*, shared-bdd-red*. These cover empty values, sorted quoted entries, source statuses, scoped ownership and member replacement history.

  • CLI configured subprocess runner executes all five read verbs against the actual unregistered CLI: 29/29 fail before production registration. Logs cli-red-build.log, cli-red-test-build.log, cli-red.log.

Shared query green checkpoint

Core 131/131 and IO/BDD 170/170 pass using executable configured runners, with physical temp paths for filesystem tests. Both builds have zero warnings/errors. Logs core-green-build.log, core-green.log, shared-bdd-green-build.log, shared-bdd-green.log. The shared API reference is docs/content/docs/configuration/inspection.md. Existing DocumentResolution and cache-only APIs remain intact; additive detailed resolution retains canonical and applied layers without extra reads.

Additional behavior checkpoints

Selected-personal query-scope regression failed 1/10 before preserving its discovered scope after promotion to the selected layer. Core then passes 134/134. Shape-blocking and composite-winner explanations and safe parser-diagnostic presentation also pass. Logs selection-red*, selection-green*.

CLI initially passes 34/35 BDD subprocess cases and 9 observer cases; the remaining deliberately red case proves unrelated malformed appsettings were loaded by the builder during help. The help-only builder path disables host defaults; ordinary execution defaults remain compatible. Log cli-host-help-red.log records that failure before the fix.

Microsoft adapter checkpoint

Adapter managed tests pass 22/22 after runtime RED13/13 and boundary RED5/22. Added tests prove earlier host providers cannot leak stale array indices or descendants under replacement values; unrelated host siblings remain. Empty .NET key segments are rejected alongside case/separator collisions. Builds are warning-free. Authored adapter coverage is 118/119 lines, 99.16%.

The non-CLI managed, trimmed and native hosts each pass 43 checks, including concrete source-generated/explicit binding, missing versus empty arrays, nested values, collision diagnostics, canonical access and bounded native-tree loading. See tests/Morphir.Configuration.Microsoft.Smoke/README.md for exact commands. Logs microsoft-publish-trimmed.log and microsoft-publish-native.log retain 44/47 pinned CsToml/FSharp.Core warnings respectively; no new adapter/binder/serialization diagnostics occur. Runtime qualification uses ILLinkTreatWarningsAsErrors=false; strict warning-free publishing remains unsupported by those pinned dependencies. Publish RID lockfile churn was restored from exact prepublish bytes.

CLI, numeric JSON and host-help checkpoint

Full configured runners now pass core 135/135, IO/BDD 171/171 and tooling 299/299. Tooling includes 38 subprocess BDD cases and 16 observer tests. Every JSON subprocess report validates docs/spec/schemas/config-inspection-result.schema.json. The actual CLI default registration is exercised, not a test-only command tree. Existing IR/cache/management behavior remains covered by the full tooling suite. Logs final-core.log, final-io.log, final-tooling.log.

Additional runtime RED checkpoints cover four empty selectors reaching discovery, invalid environment profile names reaching malformed source reads, unpaired Unicode escapes throwing instead of reporting usage, and a discovered NaN-bearing member rejected by F# structural equality. Empty selectors and profile preflight now fail before the host edge. Key-path parsing catches invalid surrogate decoding. Loading accepts the actual discovered record by identity before its compatibility structural-equality check. Public regression and both NaN CLI scenarios pass; no fixture was weakened. Logs cli-empty-controls-red.log, cli-environment-profiles-red.log, unicode-red.log, nan-member-red.log.

The user confirmed the typed JSON detail for NaN/infinities. Nonfinite numbers are tagged {"$number":"NaN"}, {"$number":"Infinity"} or {"$number":"-Infinity"}. A separate envelope valueTypes JSON Pointer discriminator identifies actual numbers. Ordinary tag-shaped objects carry no number metadata; no user key is reserved. Finite values retain JSON types, temporal kinds remain metadata, and shared redaction precedes encoding. serializer-red.log fails in Utf8JsonWriter before implementation. The actual CLI DTO source is linked into a small host with reflection serialization disabled, not duplicated. Managed, trimmed and native hosts each pass 21 checks, including all nonfinite cases, tag-shaped user objects, nested arrays, pointers, redaction and contribution/entry metadata. See tests/Morphir.Configuration.Inspection.Smoke/README.md for commands. Its trimmed/native logs retain 42/47 FSharp.Core warnings and no new serializer diagnostics. Exact prepublish lockfile bytes were restored.

Final local validation

The complete solution restore/build succeeds with zero errors and existing package/generated-code warnings, NU1608, NU1510 and CS0436. The initial full build reports 19 warnings; the incremental build after Unicode handling reports 9. This differs from the earlier baseline’s 20 due to which projects/generated sources compile, not a claim of warning-free dependencies. New core/IO/Microsoft and serializer-host builds are warning-free. Logs solution-build.log, final-solution-build.log. dotnet format Morphir.slnx --no-restore --verify-no-changes --verbosity quiet and Fantomas checks of every changed F# file pass. git diff --check passes.

Configured runner evidence is core 135, IO/BDD 171, Microsoft 22 and tooling 299, all passing. Eleven broader suites also pass: C# core 237, internal generation 25, models/roundtrips 791, pipeline 299, SDK 175, SDK conformance 27, pipeline plugins 141, F# backend 333, Ion 463, Myriad 4 and packaged Myriad integration 12. Logs broader-*.log and broader-results.json record each result. The packaged integration suite was requalified with one worker and a temporary dotnet wrapper adding -m:1 -p:BuildInParallel=false to nested build/pack operations, log packaged-consumer-serial.log. Earlier broad execution used two bounded TUnit workers, allowing internal integration builds to overlap before this corrected qualification. No unrelated process was stopped.

Current-source coverage union from core-coverage/*/coverage.cobertura.xml and io-coverage.cobertura.xml is core 3413/3565, 95.74%, and IO 639/662, 96.53%, above prior 95.38%/96.51%. The adapter’s authored source is 118/119, 99.16%. Do not combine older core dependency coverage from the Microsoft checkpoint with newer source-line mappings.

Representative reproducible commands, run serially with physical temp environment:

export DOTNET_PROCESSOR_COUNT=2
export TMPDIR=/private/tmp/morphir-config-inspection
export TEMP="$TMPDIR" TMP="$TMPDIR"
dotnet build Morphir.slnx -m:1 -p:BuildInParallel=false
dotnet tests/Morphir.Configuration.Tests/bin/Debug/net10.0/Morphir.Configuration.Tests.dll --sequenced
dotnet tests/Morphir.Configuration.IO.Tests/bin/Debug/net10.0/Morphir.Configuration.IO.Tests.dll --maximum-parallel-tests 1 --coverage --coverage-output-format cobertura --coverage-output "$TMPDIR/io-coverage.cobertura.xml"
dotnet tests/Morphir.Tooling.Tests/bin/Debug/net10.0/Morphir.Tooling.Tests.dll --maximum-parallel-tests 2
dotnet test tests/Morphir.Configuration.Tests/Morphir.Configuration.Tests.fsproj --no-build --no-restore -m:1 --collect:"XPlat Code Coverage" --results-directory "$TMPDIR/core-coverage"
/usr/bin/python3 scripts/configuration/summarize-coverage.py "$TMPDIR"/core-coverage/*/coverage.cobertura.xml "$TMPDIR/io-coverage.cobertura.xml"
dotnet format Morphir.slnx --no-restore --verify-no-changes --verbosity quiet
dotnet fantomas --check src/Morphir.Configuration/Documents.fs src/Morphir.Configuration/Resolver.fs src/Morphir.Configuration/Inspection.fs tests/Morphir.Configuration.Tests/InspectionTests.fs

Published host commands and dependency diagnostics are recorded in both smoke READMEs. Managed/trimmed/native Microsoft hosts pass 43 checks each; managed/trimmed/native serializer hosts pass 21 checks each. All stdout receipts parse as JSON and stderr receipts are empty. Canonical typed metadata remains available separately from the safe string projection.

Main was fetched again after validation and remains 23d274d4c87b367fdc79bd21843a9fdf5d74b13d, so there is no interacting base advancement. Code checkpoint 5518fb9 contains the implementation, regression fixes and serializer. Later delivery commits integrate documentation, the configured runner and archive. Final PR/head/base identities are supplied in the parent handback and forge metadata.

Specifications and parent handback

Apply was refreshed at each fulfilled task boundary and finally reports all_done, 8/8 tasks complete, in apply-all-done.json. Both new capabilities were synchronized from the approved deltas, including the confirmed nonfinite JSON scenarios. The new main specs are openspec/specs/configuration-inspection/spec.md and openspec/specs/dotnet-configuration-integration/spec.md. Their purposes and requirement/scenario bodies were compared byte-for-byte with the deltas after canonical title/Requirements-header normalization. No delta-operation headers remain in main specs.

Only add-config-inspection is archived at openspec/changes/archive/2026-11-03-add-config-inspection/; original .openspec.yaml, design, proposal, deltas and all checked tasks remain reviewable. Prerequisite/document links and the active editing prerequisite link were repaired. Editing, classic migration and workspace migration remain active. The archive CLI reports completed tasks with one nonblocking proposal suggestion to split more than ten deltas; this approved cohesive campaign remains one PR.

mise run openspec:verify passes managed-file checks, html-design schema validation, all 22 active/spec items and both extension tests. Evidence is openspec-verify.log; spec-sync-validate.log and archive.log record sync/archive checks.

The configured Nuke runner now includes the Microsoft suite and the linked-source serialization smoke using existing compiled artifacts. No new CI workflow or release/deployment behavior is added. New adapter/binder dependencies are pinned at Microsoft.Extensions.Configuration/Configuration.Binder 10.0.14; intentional central-transitive lockfile updates are retained, while publish-only RID/ILCompiler changes are restored. NOTICE and README describe the additive contribution and consumption paths.

Main was fetched immediately before PR preparation and remains the validated base. The checkout is retained for parent review and is committed clean for handback. All reproducible test/publish logs, coverage reports, smoke binaries and exact prepublish lockfile backups belong to this campaign under /private/tmp/morphir-config-inspection/; no unique source edits reside there. Parent owns PR review/current-head CI, merge/landed verification and owned worktree/local-branch/remote-branch/temporary-artifact cleanup. The campaign remains incomplete until those gates are fulfilled. No watch, merge, auto-merge, admin bypass, force-push, branch/worktree deletion, release or deployment was performed.

Final published-host refresh

After the NaN selection and Unicode fixes, the adapter was rebuilt/published from final delivery checkpoint 569e7cf8fe1e7fc5ca8e2a9001e7cd3ac3895a72. Managed/trimmed/native receipts all pass 43 checks, and the final adapter suite passes 22/22. Logs final-microsoft-tests.log, final-microsoft-publish-trimmed.log, final-microsoft-publish-native.log; JSON/stderr receipts use final-microsoft-{managed,trimmed,native}. Warning counts remain 44/47, exclusively pinned dependencies, with no new adapter/binder/serializer diagnostic. Captured lockfiles were restored exactly and the checkout was clean. The subsequent commit changes this receipt only. Final head/base/PR identities are recorded in the linked tracking issue and parent handback.

Parent review regressions and qualification

Two current-head bot findings were reproduced before correction:

  • ca5b1ef: ordinary effective and adjacent-user queries returned success despite discovery.invalid_default from automatic selection. Two subprocess scenarios failed with exit 0 rather than 2. The native inspection host now retains selection diagnostics for effective/project/user scopes. Four additional scenarios verify workspace, global, system and file scopes bypass unrelated automatic member selection. The full tooling runner passes 305/305.
  • 082e66f: a file-scoped classic manifest query included the synthetic checkpoint default because its default layer shares the manifest path. A public query regression failed before matching excluded Defaults roles. Get/list/show/path/explain now inspect the actual file layer; effective compatibility defaults and explicitly authored values remain available. The complete core runner passes 136/136.

The refreshed full solution build succeeds with zero errors and 20 existing package/generated-code warnings. IO/BDD passes 171/171 and the Microsoft adapter suite passes 22/22. The actual CLI DTO smoke adds three file-scope assertions; managed, trimmed and native osx-arm64 hosts each pass all 24 checks with reflection serialization disabled. New adapter/binder/serializer diagnostics remain absent; the documented FSharp.Core warnings remain visible under explicit warning qualification. Publish-generated lockfile churn was restored.

Fresh configured coverage reports under parent-review union to core 3417/3567, 95.79%; IO 639/662, 96.53%; Microsoft adapter 118/119, 99.16%. The summary is parent-review/coverage-summary.json. Builds, subprocess tests and publishing were bounded to two logical processors and serial heavy jobs. Both selection and file-scope behavior are documented in the CLI and shared query references. OpenSpec verification passes all 22 active/spec items, managed schema checks and both extension tests. Changed-file formatting and whitespace checks pass.

All new evidence is owned under /private/tmp/morphir-config-inspection/parent-review. Current-head CI and the independent query/provider reviews remain pending, along with main landing and owned artifact cleanup. Blanket admin-bypass authorization from the user remains in effect after checks and verified review fixes complete.

Parent provider review fixes

The independent provider review found two host-boundary defects at 45196ca. Machine configuration candidates were all assigned the invocation volume, so Windows invocations on D: could not read ordinary C: system/user locations or redirected UNC roaming locations. MachineBoundaries now plans distinct roots without observing them, and the native host explicitly authorizes those roots. ProjectLoadOptions.MachineTrees routes candidates within the supplied hosts; the existing single-tree API remains compatible. No project boundary is widened, and missing authorization still yields a structured read error without a read. Eight portable regressions cover cross-drive/UNC reads, relative MORPHIR_HOME, Unix root planning, invalid relative locations, missing authorization and isolation. The initial build failed on the missing multi-root contract; all eight now pass.

Two new CLI BDD cases reproduced process aborts, exit 134, when malformed host appsettings were present. The builder retains supported initialization failures for an opted-in configuration provider using a minimal fallback host. Commands validate arguments and profile controls before rendering a generic config.load failure, one JSON result and exit 2. Invalid arguments retain config.usage. Help remains read-free and other command defaults retain their existing behavior. Both initially failing subprocess cases now pass. Logs are host-red.log, host-green.log, machine-red-build.log and machine-green.log in parent-review.

Final host-fix validation passes core 136/136, IO/BDD 179/179, Microsoft adapter 22/22 and tooling 307/307. The tooling suite includes the two new initialization subprocess regressions. An initial broad tooling run used macOS’s symlinked default temporary path and failed filesystem boundary checks; rerunning with physical TMPDIR, TEMP and TMP passed all 307 cases. No product change was made for that environment issue. Full solution build succeeds with zero errors and 9 existing warnings on this incremental invocation. Current-source coverage union is core 3420/3567, 95.88%, and IO 662/685, 96.64%; the unchanged Microsoft adapter retains its 118/119, 99.16% qualification.

The IO smoke now exercises both cross-drive and UNC machine hosts in managed, trimmed and native osx-arm64 binaries, plus all 228 JSON5 syntax cases and 26 pinned Morphir fixtures. Each receipt reports machine_roots: true, success and empty stderr. Existing CsToml/FSharp.Core linker diagnostics remain visible under the documented warning qualification; no authored IO/boundary warnings were introduced. Publish-only lockfile changes were inspected and restored. Logs and receipts are owned under parent-review, including host-final-build.log, io-host-full.log, tooling-host-full.log, machine-*-publish.log and machine-*-result.json. Changed-file formatting and whitespace checks pass.

OpenSpec verification was refreshed after the host fixes and passes all 22 active/spec items and both extension tests. Current-head CI and the independent query review remain pending before the authorized landing and cleanup.

Windows CI follow-up

The failed Windows job in run 41624211494 used the superseded 45196ca head. Twenty-one non-isolated subprocess cases failed with C:/ProgramData read errors from the D: invocation-volume authorization bug, fixed in 740aeba. One isolated explanation case passed execution but compared canonical ‘/’ source paths with native Windows backslash paths. Both the explanation and origin assertions now construct expected canonical paths through ProjectPath.join. This changes only test expectations; public report paths retain the shared canonical contract. The Ubuntu/macOS jobs were cancelled by the matrix’s default fail-fast behavior after Windows failed. Current-head checks must qualify the replacement commit; none of those older results are treated as current-head evidence. The downloaded failure log is retained under parent-review/ci-windows-45196ca.log.

The affected explanation and origin subprocess cases each pass after this test-only correction, and the tooling test build, changed-file formatter and whitespace checks pass. Native Windows confirmation remains CI-owned.

Parent query review fix

The independent query review found that supported deep values failed inspection presentation at 45196ca: readers accept 128 levels, while both the intermediate JsonDocument and generated report serializer retained System.Text.Json’s default 64-level limit. A linked-source smoke regression first failed with a reader-depth exception, depth-red.log. Raising only the value writer/parser limit then failed at the report envelope’s generated serializer, depth-envelope-red.log. The common 256-level limit now covers the writer, JsonDocument and generated context, allowing the reader limit plus report nesting and typed numeric tags.

The smoke host adds 24 checks across 65- and 127-segment nested objects through show, subtree get and explain. It checks successful JSON, deepest-leaf numeric tags/type metadata and redaction before encoding. Managed execution passes all 48 checks. Two additional CLI BDD cases load real TOML at those same depths and validate the published JSON schema, leaf values, exit code and redaction. Both pass. The CLI reference documents an appropriate depth limit for consumers. The disposable review probe directory is /private/tmp/morphir-config-inspection-review-queries.

Depth qualification is complete: managed, trimmed and native osx-arm64 serializer hosts each pass all 48 checks with reflection serialization disabled and empty stderr. The full tooling suite passes 309/309, including 48 subprocess scenarios. Full solution compilation and changed-file formatting pass. Only documented FSharp.Core dependency linker diagnostics remain; publish-only lockfiles were inspected and restored. Receipts are parent-review/depth-*-result.json.

Integration with updated main

Main advanced to 4df3c1ad425f782bfe3cc02e30f577d87dca0f53 with the canonical F# SDK package migration, PR #469. The SDK runtime moves to a canonical project while preserving its namespace and assembly identity. It does not change the configuration libraries, readers, Microsoft adapter or serializer sources, so their native qualification remains applicable. The solution/README auto-merged; NOTICE had an append conflict, resolved by preserving the entire SDK notice followed by the entire configuration notice. No history was rewritten.

The combined tree builds with zero errors and 20 existing warnings. All 16 configured runners pass sequentially with one TUnit worker, sequenced Expecto, two logical processors and a dotnet wrapper serializing nested builds/packs. Results: C# core 237, tooling 309, configuration core 136, IO/BDD 179, Microsoft adapter 22, serializer 48 checks, internal generation 25, model/roundtrips 791, pipeline 299, SDK 403, SDK conformance 27, pipeline plugins 141, F# backend 333, Ion 463, Myriad 4 and packaged Myriad integration 12.

Two SDK package-consumer checks initially failed because the installed PowerShell shim had no selected version. Process-local selection of PowerShell 7.5.3 allowed them to run; the final ownership check then found four stale SDK DLLs under this checkout’s pre-migration ignored bin/obj outputs. Those two ignored directories contained no tracked files and were removed. The unchanged SDK suite then passed 403/403, including both package versions and all canonical, legacy, combined, generated-source and precompiled-binary consumers. No SDK product code or global tool configuration was changed for these environment issues. Original failed receipts remain separate from the successful rerun.

parent-review/integrated-final-results.json indexes the final receipts, including integrated-sdk-final.log. The first unselected-shim attempts named two disposable paths outside the main evidence directory; neither contains product work: /private/tmp/morphir-fsharp-sdk-packages-a13f59919fc0435dbd9ec893900a2f45 and /private/tmp/morphir-fsharp-sdk-packages-f52a160b844543a4859e20842d67a7c9. Other package evidence is under the owned parent-review/sdk-package-temp. OpenSpec verification on the combined tree passes all 22 active/spec items and both extension tests. Both independent reviews are complete and their reported findings are fixed. Fresh current-head CI, landing and artifact cleanup remain.

Concurrent schema-validation harness fix

Current-head macOS run 41630213426, head 8118ff0, failed one tooling BDD case, quoted literal-dot paths, while constructing the published inspection schema. JsonSchema.Net reported RefResolutionException for a generated local URI ending in #/$defs/entry. The CLI execution itself had succeeded. All other suites passed; matrix fail-fast cancelled Ubuntu and Windows. The full failed job log is parent-review/ci-macos-8118ff0.log.

Inspection scenarios had called JsonSchema.FromText without BuildOptions, sharing the library’s global registry during concurrent schema construction. The pinned JsonSchema.Net 8.0.5 source uses an unsynchronized Dictionary for that registry. Both the pinned BuildOptions source and registry source confirm this shared mutable path; the author’s documentation recommends separate registries for concurrent builds or conflicting registrations. The test harness now supplies a fresh SchemaRegistry per schema, matching the existing production SchemaLoader pattern. No CLI/product, schema, dependency, workflow, coverage or scheduling policy changes are involved.

The race is intermittent: the unchanged local 48-case BDD run with four workers passed, and a 512-build global-registry probe did not reproduce the CI failure. The actual CI failure remains the red evidence. After isolation, all 309 tooling tests pass with four workers, including all 48 configuration subprocess cases, and an isolated 512-build probe passes. Test compilation, formatting and whitespace checks pass. Successful receipts are schema-registry-tooling-full.log and schema-registry-probe-isolated.log; original baseline receipts remain separate. The earlier integrated full-suite and native qualifications still apply because this fix only changes the test schema-construction boundary. Fresh current-head CI remains required.

Integration with C# SDK main update

Main advanced again to 55f73bdaa93b7bc55a48b184bab8e46a5636fd9e, the immutable C# SDK runtime from PR #470. The solution, README and configured test-runner changes auto-merged. NOTICE had an append conflict; both complete notices are retained, with main’s C# SDK notice at the end to keep its append point intact. No configuration code, test registry fix, dependency pin or generated serializer source was changed or discarded, and no history was rewritten.

The combined solution builds with zero errors and 20 existing warnings. All 17 configured runners pass sequentially with two logical processors, one TUnit worker and sequenced Expecto. This includes the new C# SDK 20/20 suite, F# SDK 403/403 including package consumers, core configuration 136, IO/BDD 179, Microsoft adapter 22, tooling 309, serializer 48 checks, C# core 237, internal generation 25, model/roundtrips 791, pipeline 299, SDK conformance 27, pipeline plugins 141, F# backend 333, Ion 463, Myriad 4 and packaged Myriad integration 12. Process-local PowerShell selection and serial nested builds were configured from the beginning; no environment workaround or retry was needed in this run. The concurrent 309-test tooling qualification for the registry fix remains separately recorded above.

Evidence is owned under parent-review/integrated-55f73bd/, with all 17 successful exit codes in integrated-results.json; the full build and verification receipts are integrated-55f73bd-build.log and integrated-55f73bd-openspec.log in parent-review. OpenSpec passes all 22 active/spec items and both extension tests. Whitespace checks pass. The SDK addition has no references from the configuration libraries or their smoke hosts and changes none of their sources, dependencies, target frameworks or publish options, so the earlier managed/trimmed/native configuration qualification remains applicable. Current-head CI, landing and owned cleanup remain pending.

16.1.6.11 -

Classic project migration delivery journal

Tracking issue: #476. Change: add-classic-project-migration, campaign 5 of six.

Ownership and baseline

  • Owned checkout: /Users/damian/.t3/worktrees/morphir-dotnet/t3-5e544978.
  • Owned branch: t3/classic-project-migration, externally renamed from the prepared t3/5e544978 at the same baseline. Initial status was clean.
  • Required and verified baseline: 56d9de917f6c3035c8a2f994f49dfc96294619d8.
  • Discovery PR #467 landed at 23d274d4c87b367fdc79bd21843a9fdf5d74b13d, an ancestor of the baseline.
  • Editing PR #475 landed at the baseline. Its completion receipt confirms validated tree, checks, archive and cleanup.
  • OpenSpec html-design was ready, 0/8 implementation tasks. All returned context files, including the full design, were read before implementation.
  • Parent owns independent final review, CI watching, authorized landing and cleanup. This worker will not watch, merge, delete branches/checkouts or begin workspace migration. Campaign completion remains pending those parent gates.

Work and evidence ownership

Two Codex helpers own the pure F# planner/tests and CLI/provider/subprocess tests. The lead owns IO coordination, integration, documentation, qualification and delivery. Helpers preserve one another’s edits and do not run heavy builds independently.

All owned external artifacts are beneath /private/tmp/morphir-project-migration/. Physical tmp/ is used for TMPDIR/TEMP/TMP. Builds use DOTNET_PROCESSOR_COUNT=2, serial MSBuild and disabled parallel build/restore. Executable Expecto uses --sequenced; TUnit uses --maximum-parallel-tests 1. One heavy job runs at a time. Unresolved recovery evidence must be preserved.

Checkpoints

  • Setup verified clean checkout, branch ownership, prerequisite ancestry and authoritative editing receipt. Actual tracking issue #476 exists before coding. No runtime changes at this checkpoint.

  • Mapping RED: core-red-mapping.log exits 1 for the missing pure migration API before its implementation. IO BDD RED: io-red.log exits 1 for the missing preview service before its implementation.

  • CLI RED: the actual CLI output was rebuilt successfully, then cli-red-run.log runs 27 subprocess BDD cases, all failing for the missing project command. An earlier class-name filter selected zero tests and is not evidence; the corrected category filter is /*/*/*/*[Category=ProjectMigration].

  • Equivalence RED: after mapping compiled, core-red-verification-02.log fails for the missing ProjectMigration.verify before verification implementation. A preceding reserved-word syntax failure was fixed and is not counted as behavioral evidence.

  • Base workspace pipeline settings may override classic low-priority checkpoint defaults. planWithContext uses the base-only before snapshot to materialize the effective legacy defaults, preserving member behavior rather than forcing checkpoint over a declared workspace choice.

  • Core GREEN: core-green.log passes 170/170 Expecto cases; core-green-02.log passes 172/172 after explicit conflict-values and base-workspace evidence regressions. core-red-conflict.log first proved those APIs absent. Core builds report no warnings/errors.

  • IO io-green-03.log passes 296/296 TUnit/Reqnroll cases. Equal-value text/BOM checks preserve original spans; insertion can precede a table, so the test checks retained text rather than asserting an unchanged file prefix. Native cases cover adjacent and all supported primary layouts, source retention, repeat timestamps, source/target/workspace/alternate-primary drift and read-only/foreign-plan refusal.

  • CLI cli-green-02.log passes 34/34 focused command and actual subprocess cases after rebuilding src/Morphir/bin. The preceding run passed 32 and exposed absent conflict values plus a missing option value consuming --json; both regressions are green. Every subprocess JSON report is validated against a fresh schema registry, including initialization and parse failures.

  • A read-only host receives safe complete data through Presentation, while canonical plan data is explicitly separate. The planner has no CLI dependency. No existing cache-only API or owner/personal/machine-loading policy changed.

  • The full IO executable now passes 303/303 cases in validation/Morphir.Configuration.IO.Tests.log, including migration post-install rollback and explicit retained-recovery outcomes. Fault fixtures verify transaction ownership and backup bytes before manual recovery, then remove only resolved owned evidence. No unresolved recovery artifact remains from those checks.

  • Source/target/relevant workspace evidence is rechecked by the existing transaction service. Application rejects unverified, read-only and foreign-service plans. Preview never acquires writer coordination or stages bytes. The classic source is never a transaction destination.

Final local qualification

All 17 configured executable runners passed serially. validation/summary.json records each exact command and exit status. Additional boundary tests then passed in the final focused runners, increasing core from 172 to 175 and IO from 303 to 307 without changing production behavior. The adapter coverage invocation is a coverage collector, not a substitute for the configured sequenced Expecto gate.

RunnerPassed
Morphir.CodeGeneration.FSharp.Tests333
Morphir.Configuration.IO.Tests307
Morphir.Configuration.Inspection.Smoke71 checks
Morphir.Configuration.Microsoft.Tests22
Morphir.Configuration.Tests175
Morphir.Core.Tests237
Morphir.IR.Pipeline.Plugins.Tests141
Morphir.IR.Pipeline.Tests299
Morphir.Internal.CodeGeneration.Tests25
Morphir.Ion.Tests463
Morphir.Models.Tests791
Morphir.Myriad.Integration.Tests12
Morphir.Myriad.Tests4
Morphir.SDK.CSharp.Tests20
Morphir.SDK.Conformance.Tests27
Morphir.SDK.Tests403
Morphir.Tooling.Tests397

The full solution build succeeds. The first broad build reports 18 existing package/generated-code warnings, and the final analyzer-enabled build reports 11 existing warnings with zero errors. Changed-library/test builds report zero warnings/errors. C# formatting and full solution formatter verification pass; Fantomas formats and checks the new F# source/tests. No parser, SDK or dependency version changes are included.

coverage-final-summary.json unions the final current-source reports from core-boundary-coverage/*/coverage.cobertura.xml and io-boundary-coverage.cobertura.xml. Core is 4178/4335, 96.38%; IO is 1553/1599, 97.12%. Both improve on the prerequisite’s 96.01% and 96.94%. Added cases qualify safe read-only presentation, equal typed floats/references, invalid Unicode, invalid/missing source states, changing mounted targets and unstable storage. The full Models runner includes 791 model/codec roundtrip checks. IO contracts, JSON result-schema subprocess checks and pinned upstream fixtures also pass. The scripts/format-all, scripts/test-all and scripts/check-contracts placeholders listed in AGENTS.md are absent; the configured commands above supply their gates.

Non-CLI managed, trimmed and native hosts

The IO smoke host is extended with migration and linked-source generated report serialization. All three osx-arm64 publishes execute successfully with reflection serialization disabled. Each reports 24 migration checks, 44 existing editing checks, 228 official JSON5 syntax cases, 26 pinned Morphir documents and the existing discovery/machine-root/owner checks. Published stderr is empty. file identifies the native output as Mach-O 64-bit arm64.

publish/summary.json retains exact argument arrays, exit codes and warning counts. Managed publishing has zero warnings. Trimmed/native publishing retains 44/47 warning lines from pinned CsToml and FSharp.Core. Each explicitly sets ILLinkTreatWarningsAsErrors=false; no diagnostic is suppressed and no strict warning-free trimming claim is made. Authored migration source has no linker/AOT diagnostic. Per-file atomic installation and machine-local writer coordination remain the existing limits; hosts needing distributed coordination supply it.

publish-locks/manifest.json records prepublish lockfile SHA-256 values, and restored.json confirms every captured file restored byte-for-byte. Native publication introduces no lockfile/dependency change. All artifacts are local qualification outputs; no release, deployment or external publication occurred.

Reproduction

Run one heavy job at a time from the repository root:

export DOTNET_PROCESSOR_COUNT=2 BuildInParallel=false
export TMPDIR=/private/tmp/morphir-project-migration/tmp
export TEMP="$TMPDIR" TMP="$TMPDIR"
export PATH="/Users/damian/.local/share/mise/installs/powershell/7.5.3:$PATH"
dotnet build Morphir.slnx -m:1 -p:BuildInParallel=false -p:RestoreDisableParallel=true
dotnet tests/Morphir.Configuration.Tests/bin/Debug/net10.0/Morphir.Configuration.Tests.dll --sequenced
dotnet tests/Morphir.Configuration.IO.Tests/bin/Debug/net10.0/Morphir.Configuration.IO.Tests.dll --maximum-parallel-tests 1
dotnet tests/Morphir.Tooling.Tests/bin/Debug/net10.0/Morphir.Tooling.Tests.dll --maximum-parallel-tests 1
# Repeat the configured arguments from validation/summary.json for the other runners.
dotnet format Morphir.slnx --no-restore --verify-no-changes --verbosity quiet
dotnet fantomas --check src/Morphir.Configuration/Migration.fs tests/Morphir.Configuration.Tests/MigrationTests.fs

Coverage collectors and publish commands follow the preceding configuration editing journal and the updated IO smoke README. The owned run-suites.py, format.py and publish-host.py scripts retain the exact local execution. PowerShell 7.5.3 is selected only through the process-local PATH for package tests. No global tool configuration changed.

Scope and recovery audit

no-config-alias-receipt.json records that actual config migrate --json returns structured exit 2. The implementation registers only project migrate. There is no IR/source conversion, sibling batch, workspace adoption or membership edit. Classic manifests remain byte-identical. Existing cache-only and public loading APIs remain compatible; selected personal and machine ownership are unchanged. Native recovery fault fixtures resolve their verified backups explicitly. No unresolved recovery artifact remains from local qualification.

Specification sync and parent handoff

OpenSpec apply reports all_done, 8/8 implementation tasks. Only add-classic-project-migration is synchronized and archived at openspec/changes/archive/2026-11-03-add-classic-project-migration/. The new main capability is openspec/specs/classic-project-migration/spec.md. Its Purpose and all nine requirement bodies with 11 scenarios are byte-identical to the approved delta, with canonical main-spec headings. The archived task validator passes, the original .openspec.yaml remains present, and the design has balanced HTML, inline CSS and valid local links. Archive-relative prerequisite and PRD links are repaired. Workspace migration remains active at 0/9 tasks; only its prerequisite link changed.

mise run openspec:verify passes before and after archive: the managed html-design snapshot, all 22 active/spec items and both extension tests. Strict main-spec validation passes 17/17; archived-task validation passes 10/10, including this archive. The CLI rejects path separators in direct item names, so archived delta content is qualified through its exact match to the strict-validated main spec, the pre-archive strict validation and the dedicated archived-task gate. The unsupported direct-path command receipt is preserved explicitly, without being counted as a successful validation.

The final PR targets main, still at the required baseline 56d9de917f6c3035c8a2f994f49dfc96294619d8 when checked after qualification. The PR body and parent handoff record the actual final head separately. This journal is implementation delivery evidence, not CI/review approval or a campaign-completion receipt. Parent owns independent review, CI watching, authorized landing and cleanup. No watch, merge, branch/worktree deletion, workspace migration, release or deployment was performed by this worker.

Owned artifact inventory

All external artifacts are owned beneath /private/tmp/morphir-project-migration/:

  • tmp/: physical temporary root used by every owned test/build subprocess.
  • validation/: 17 configured runner logs and exact command/result summary.
  • core-coverage/, core-boundary-coverage/ and IO Cobertura files: collector evidence, with coverage-final-summary.json selecting the final source maps.
  • publish/: managed, trimmed and native hosts, build logs, JSON results, stderr and exact argument/warning summary.
  • publish-locks/: original lockfile bytes, SHA-256 manifest and restoration proof.
  • Root-level red/green/build/format/OpenSpec receipts and reproduction scripts, plus PR handoff text and the generated artifact-inventory.json.

Generated bin, obj and TestResults within the retained owned checkout are also campaign artifacts. Shared package/tool caches and unrelated worktrees are not cleanup targets. No unique source work is held in external artifacts; all source, tests, documentation and spec changes are committed in this PR. Recovery fault fixtures were resolved explicitly; the final inventory records no unresolved native recovery files. Preserve any later unresolved evidence during parent review. The checkout and branch are intentionally retained for parent landing.

Parent review follow-up

Independent core review of 5035f21 found two actionable P2 issues. Declared profile controls disappeared from both disabled-activation effective trees, allowing a missing or changed declaration to verify. Invalid Unicode ancestor keys were skipped when raw extras were flattened into leaf ledger entries. Both were reproduced before production changes. parent-core-red.log records 26 passing and two failing focused tests.

Verification now compares declared root profile controls using the existing raw control merge rules, without activating profiles or requesting profile files. Those controls are included in the redacted before/after snapshots and explicit differences. The planner validates every key path segment before accepting a source ledger entry. Missing/changed default, active, include and group controls, and invalid top-level/ancestor/leaf keys have regression coverage. parent-core-green.log passes all 177 core tests. The published migration host also exercises unchanged/missing profile declarations and an invalid JSON5 ancestor key. The rebuilt solution passes with the same 11 existing warnings.

Current-head integration, formatting, coverage and runtime qualification results are recorded below when complete. Parent review/CI/landing and cleanup remain campaign gates; this follow-up is not a completion receipt.

All 17 configured executable runners passed again after the parent fixes, using freshly rebuilt outputs and the same serial resource limits. Core now passes 177 cases, IO 307, tooling 397 and Microsoft 22; all other counts remain as in the original qualification table. validation/summary.json contains the refreshed command, exit-code and duration evidence. The full Models runner again passes all 791 model/codec roundtrips.

Parent qualification after 98f1d39 passes full C# formatter verification, Fantomas including the shared profile helper, OpenSpec managed/extension checks, and fresh core/IO coverage collectors. parent-coverage-summary.json uses only current-source reports: core 4186/4343, 96.38%; IO 1553/1599, 97.12%. All three freshly published managed, trimmed and native osx-arm64 hosts pass 27 migration checks, 44 editing checks, 228 JSON5 cases and 26 Morphir fixtures. Published stderr is empty. The trimmed/native dependency warning counts remain 44/47 with explicit normal-warning qualification; no diagnostic suppression was introduced. Publish-only lockfiles were restored byte-for-byte. Exact commands and exit codes are in parent-qualification-summary.json, publish/summary.json and publish-locks/restored.json under the owned artifact root.

Directory ownership and explicit primary anchors

Independent IO/CLI review found that inventory selection could migrate an unrelated sole sibling when a directory input named an unlisted project. ownership-red.log reproduces five failing API cases, including a containing root project. ownership-cli-red.log passes 34 existing CLI cases and fails all four new unlisted apply/preview/current/nested subprocess cases before the fix. Migration now requires a selected project to own the requested directory and rejects any nearer inspected primary that belongs to an undeclared project. Workspace-root unambiguous selection and declared-member subdirectories remain supported. Initial green runs pass 314 IO cases and all 38 migration CLI cases.

A related parent probe found that explicit .morphir/morphir.json input used .morphir as its project anchor. layout-anchor-red.log passes the adjacent and dot-config layouts and fails the hidden layout’s project-location assertion. Explicit primary inputs now use shared candidate policy to identify the owning project directory before discovery. This preserves source-relative meaning and workspace member identity for every supported primary layout. Native smoke coverage includes unlisted selection and all explicit primary anchors.

After both ownership corrections, the rebuilt solution passes with nine existing warnings and zero errors. All 17 configured runners pass again from those outputs: core 177, IO 314, tooling 401 and Microsoft 22; all other runner counts remain unchanged. The complete CLI run includes the four actual subprocess refusal cases. ownership-io-final.log, ownership-final-build.log and the refreshed validation/summary.json retain this evidence. These fixes keep the shared discovery API unchanged and enforce migration’s narrower ownership contract in the reusable IO service.

Final ownership qualification after 0d07db3 passes formatter/Fantomas, OpenSpec/extension verification and refreshed coverage collectors. Current-source coverage is core 4186/4343, 96.38%, and IO 1570/1616, 97.15%, recorded in ownership-coverage-summary.json. Fresh managed, trimmed and native hosts each pass 32 migration checks, 44 editing checks, 228 JSON5 cases and 26 Morphir fixtures. Published stderr is empty. Trimmed/native dependency warnings remain 44/47 with the existing explicit warning-severity qualification. No diagnostic is suppressed. Publish lockfiles were restored byte-for-byte and no unresolved recovery remains. Exact argument arrays, exits and warning counts are retained in the refreshed parent qualification and publication summaries.

Both independent reviews of the delivered baseline are complete. All three reported findings and the related explicit-layout anchor defect have been reproduced and corrected by the parent with regression evidence. Current-head CI, authorized landing and audited cleanup remain outstanding campaign gates.

16.1.6.12 -

Project and workspace discovery delivery

Campaign 2 tracks issue #466. Owned checkout: /Users/damian/.t3/worktrees/morphir-dotnet/t3-2ee0222b. Branch: t3/add-project-workspace-discovery. Prepared and current main baseline: 3d5b41c0f839c7b2648403ad6aba5c5644f7dfb5. The initial checkout was clean. Its tree is 66609771da2eb88ac9e3cab3c21b6e8d201a6748, the verified squash tree of prerequisite PR #461. The parent completion receipt records campaign 1 landing and cleanup, including its 33 reproducible temporary artifacts. That tree is a tree object, not a commit ancestry identifier.

The parent owns PR review/check watching, merge and cleanup. This worker delivers one reviewable PR, without watching, merging, deleting branches/worktrees, force-pushing, publishing or deploying. Campaign completion remains pending until parent landing and cleanup receipts exist. Future campaigns remain active.

Validation checkpoints

  • Initial BDD scenario describes a member primary below a workspace. The capped adapter test build fails with CS0246 for the absent InMemoryProjectTree and ProjectDiscoveryResult APIs. No discovery implementation exists at this red checkpoint. Logs are in /private/tmp/morphir-discovery-tests/.
  • All builds use DOTNET_PROCESSOR_COUNT=2, single-node MSBuild, disabled parallel restore/build, and one heavy job at a time. Executable TUnit/Reqnroll uses one worker and physical macOS temporary paths. Core uses Expecto.
  • e849db3 records initial red; 7299331 records first passing API slice.
  • Root ambiguity was observed red with all 15 layout/extension cases green. Boundary group then exposed outside-start host access and missing diagnostics for a directory at a primary filename. Both regressions were made green. An incomplete classic test fixture was corrected to a canonical project after error propagation revealed its required exposedModules field was absent.
  • Membership group was observed red: 15/110 failed for globs/exclusions, root/virtual identity, workspace.root, invalid/missing literals, duplicate/path selection, defaults and bounds. After implementation adapter 110/110 passes, zero build warnings/errors. Native adapter, scoped loading and qualification remain pending. Evidence logs use red-members-* and green-members-*.
  • Machine planner first failed compilation for absent APIs, then Linux 1/1 passed. Expanded platform tests ran 14 cases with 4 pass, 7 fail, 3 error; after implementation 14/14 passed. Overlay/ambiguity missing APIs were observed red before implementation. Builds are serialized by the primary worker.

Adapter and loading checkpoints

  • bd325c2 is an external checkpoint of the then-current files, followed by owned db17ca1. Both are preserved. The source-planner and documentation helpers confirm they ran no staging or commit commands.
  • Native adapter parity/link tests first failed compilation for the missing filesystem adapter; after implementation adapter 112/112 passed. Tests compare complete immutable discovery and selection results across native and memory hosts, then compare complete loading results as well.
  • Loading first failed compilation for the absent API and options. After implementation adapter 121/121 passed. Pure core passed 124/124, including the new platform/overlay planner’s 28 cases and a resolver ordering regression. That regression failed at runtime with workspace personal TOML beating member personal YAML. The separate member-personal stage now wins across formats. The earlier substring-only Expecto filter selected zero tests; the recorded red evidence reran the full Resolution list and ran 31 cases, 30 pass/1 fail.
  • Canonical schema failures, requesting below member links and member-personal workspace output relocation produced 4 red failures, then 125/125 passed. Isolation with supplied machine sources produced 1 red failure, then 127/127 passed. Directory enumeration failure lacked source evidence in a 1/128 red run; after implementation adapter 128/128 passed. Focused builds have zero warnings/errors. Native directory probes never open discovered links.
  • Implementation tasks 2.1 through 3.2 are fulfilled. Apply instructions were refreshed after every checkbox update. Broader compatibility, runtime, coverage, formatting and final archive qualification remain pending.

Broad verification and semantic audit

  • b6ff7c5 protects bounded native/mounted loading and its initial red/green regressions. Full solution build succeeds with the same 20 existing package and generated-code warnings and zero errors.
  • All 14 configured Nuke-target suites pass serially using compiled Debug outputs: C# core 237, tooling 245, configuration core 124, adapter/BDD 128 at that checkpoint, internal generation 25, models/roundtrips 791, pipeline 299, SDK 175, SDK conformance 27, pipeline plugins 141, F# backend 333, Ion 463, Myriad 4, and Myriad integration 12. See suite-results.json and each suite’s log under the owned temporary root. TUnit uses one worker, Expecto sequenced.
  • A read-only semantic audit then produced concrete regression tests. Adapter run red-audit-run.log has 9 failures out of 137: explicit disabled profiles inherited premature discovery errors, host warning suppression leaked discovery warnings, sibling literal members with exclusions crashed, sibling globs were missed, UNC authority was lost, build-named projects were implicitly pruned, supplied default layers were falsely ambiguous, and two permitted owner-output overlays left stale context. All nine are fixed; adapter 137/137 passes.
  • A follow-up invalid owner-output overlay failed from a member load, then passed after retaining workspace validation errors in the owning context. Adapter 138/138 and core 124/124 pass. Discovery validates canonical declarations without interpreting profile activation or precomputing suppressible format warnings. Member inventories now honor within-boundary sibling literals/globs and declared build-named directories. UNC paths retain their authority. Root personal/profile output settings produce the same output context for root and member loads.
  • Fantomas formats owned F# files; C# whitespace formatting covers owned C# files. Full dotnet format Morphir.slnx --no-restore --verify-no-changes --verbosity quiet passes; unsupported F# projects are checked separately with Fantomas.
  • Final full-solution rebuild after these fixes succeeds with zero errors and the same 20 existing warnings. Configured coverage runners pass core 124/124 and adapter/BDD 153/153. The additional boundary cases cover selected-file misses, unsupported formats, outside paths, personal access/link evidence, absent machine authorization, relative Morphir home, invalid selection and native permission failures. Their source-line union is core 3010/3172 = 94.89% and authored adapter 624/647 = 96.45%, above campaign 1’s 94.13% and 94.23% rates. Reproduce with scripts/configuration/summarize-coverage.py over core-coverage/*/coverage.cobertura.xml and io-coverage.cobertura.xml.
  • Consumer audit confirms ManagementScenario and ConfigResolver still reference the unchanged legacy WorkspaceDiscovery. No existing caller was migrated to DiscoveryPolicy or ProjectSystem. Task 3.3 is fulfilled and apply reports 7/8.

Runtime and final delivery qualification

Validated implementation checkpoint: 230d1a4. The later tracked changes contain only test whitespace, runtime README and final documentation/spec synchronization. Both full-solution builds succeeded with zero errors and 20 existing warnings. The final physical-temp tooling rerun passes 245/245. Full C# format verification, Fantomas checks and whitespace checks pass. No CLI dependency or old-consumer registration was introduced.

Runtime commands are in tests/Morphir.Configuration.IO.Smoke/README.md and the exact argument arrays/results are retained in /private/tmp/morphir-discovery-tests/runtime-results.json. All three publishes use DOTNET_PROCESSOR_COUNT=2, -m:1, disabled parallel build/restore, Release osx-arm64, and the same physical temporary root. They run serially.

  • Strict trimmed publishing exits 1 on pinned CsToml IL2055/IL2067 and FSharp.Core reflection/resource diagnostics. publish-strict.log retains those errors.
  • Warning-qualified trimmed and native publishing both exit 0 with explicit ILLinkTreatWarningsAsErrors=false; normal dependency diagnostics remain visible in publish-trimmed.log and publish-native.log. No suppression or strict warning-free qualification is claimed.
  • Both published binaries exit 0 and report success, formats=3, JSON5, normalization, profile loading, host parity, discovery, selection and bounded loading, plus syntax_cases=228 and morphir_cases=26. Native/mounted results are compared structurally, active profiles remain in isolated loading, and workspace output ownership is checked. file identifies the native executable as Mach-O 64-bit arm64.
  • Publish-generated RID/ILCompiler changes in the three owned package lockfiles were restored to their saved prepublish bytes, preserving the main dependency pins CsToml 1.8.6 and YamlDotNet 16.3.0. No intentional dependency update was reverted.

Coverage reports under the same root pass 124 core and 153 reader/BDD tests. The source-line union is core 3010/3172, 94.89%, and authored IO 624/647, 96.45%. This excludes generated obj/ code and exceeds the prior campaign’s recorded rates. The coverage summary script, configured runners and arguments above reproduce those results. Optional access failures, selected-file errors, links and machine-host authorization are included in the adapter suite.

Specification and parent handback

OpenSpec apply returned all_done, 8/8 tracked tasks complete. All artifact/task checks passed before sync/archive. The new main specification is openspec/specs/morphir-project-system/spec.md; its Purpose was copied verbatim and all 11 requirements and 19 scenarios were compared byte-for-byte with the approved delta. No delta operation headers remain in the main spec.

Only this change is archived at openspec/changes/archive/2026-11-03-add-project-workspace-discovery/. Its original .openspec.yaml, proposal, design, delta and checked tasks are preserved. Prerequisite links in the two dependent proposals were updated without checking or archiving their tasks. Inspection, editing and both migration campaigns remain active, along with unrelated active changes.

mise run openspec:verify passes managed schema/snapshot checks, all 21 strict active/spec validations, and both extension tests. Main was fetched again and remains 3d5b41c0f839c7b2648403ad6aba5c5644f7dfb5; there is no base advancement to reconcile. The PR targets this current main. The final handback supplies the actual PR URL, head SHA and base SHA; local evidence is distinguished from CI.

The checkout is retained for parent review. Reproducible evidence and published binaries are owned beneath /private/tmp/morphir-discovery-tests/, with no unique source edits held there. Parent owns review/CI watch, merge/landed verification and subsequent checkout/local-branch/remote-branch/temporary-artifact cleanup. No watch, merge, auto-merge, admin bypass, force-push, branch/worktree deletion, release or deployment was performed. Campaign completion remains pending parent landing and cleanup even though implementation, local validation and archive are complete.

Parent review: selected personal ownership

A read-only adapter review reproduced an ownership bypass when an eligible personal file was explicitly selected. The member file could relocate the workspace output; a workspace personal file could supply a member’s project source directory and exposed modules. Four public loading regressions, covering ordinary and isolated loading for both owners, failed before the fix.

Loading now keeps the eligible personal source’s original role alongside its selected contribution. Isolation reads only the explicitly selected personal file, retaining that ownership metadata without enabling other personal reads. Pure resolution uses the original personal roles when scoping selected values; the owner-context pass excludes selected member-personal files. The ordinary contribution still occurs only once at selected precedence.

Both projects build without warnings. The full configured core runner passes 124/124 and the adapter/BDD runner passes 157/157. The first parent adapter run used macOS’s linked default temp path and failed three native-filesystem cases; the rerun with TMPDIR/TEMP/TMP set to /private/tmp/morphir-discovery-tests/parent-review passes all cases. Its log is /private/tmp/morphir-discovery-tests/parent-review/adapters.log. Owned F# and C# files were formatted and the working diff passes whitespace checks. Final native qualification, current-head CI and the independent discovery-policy review remain pending for these parent changes.

The parent subsequently qualified implementation 05f7555 with additional smoke checks for explicitly selected workspace/member personal files, both with and without isolation. The trimmed and native osx-arm64 hosts both pass structural native/mounted parity, owner output and member-identity assertions, and all 228 JSON5 syntax checks plus 26 Morphir documents. Publish commands match the smoke README with two logical processors, single-node builds and explicit warning qualification; outputs and logs are under the same parent-review directory. Pinned dependency warnings remain visible, and no strict warning-free claim is made. Publish-generated lockfile churn was restored to the committed bytes.

Fresh configured coverage runners pass core 124/124 and adapters/BDD 157/157. Their authored-source union is core 3030/3189, 95.01%, and adapters 636/659, 96.51%. Reports are under parent-review/core-coverage and parent-review/adapter-coverage.cobertura.xml, summarized with the existing coverage script. All eight host checks passed on the earlier 115a17d; the ownership fix requires fresh CI. The separate discovery-policy review and landing/cleanup remain outstanding.

Parent review: discovery policy conformance

The independent policy review confirmed two earlier bot findings remained actionable through the public discovery API, and reproduced a reserved-directory glob-seed bypass. The earlier topology-shape finding was already addressed: invalid project/workspace mappings and malformed member lists return structured validation errors and no accepted topology. Three additional existing-behavior test arguments retain coverage of those verified shape cases.

  • 36a07a8 assigns MemberPrimary to member inputs, embedded documents, diagnostics and reports, including evidence collected during the initial upward walk. A public API regression failed on the former RootPrimary role; it now passes member sources directly into Resolver.resolve and retains name, exposed modules and classic checkpoint defaults without adapter repair.
  • The same checkpoint adds glob-prefix reachability checks before inventory requests. Two regressions failed with access errors from excluded or impossible descendants. Both now pass, without observing those paths. Fully excluded recursive branches are pruned while potentially matching descendants retain the declared glob/exclusion semantics.
  • 25f7088 checks reserved components between the workspace declaration and a glob directory before observing it. Three red regressions for .morphir/*, .config/* and a deeper literal prefix containing .config now pass without admitting hidden projects or selecting them automatically.

The complete solution build succeeds with zero errors and 20 existing warnings. Fresh configured runners pass core 124/124 and adapters/BDD 166/166. Their authored-source coverage union is core 3100/3250, 95.38%, and adapters 636/659, 96.51%. The reports and build/test logs are under parent-review, using policy-* names. Both affected libraries compile without warnings.

The updated trimmed and native osx-arm64 smoke hosts pass direct member-source resolution with classic defaults, finite-glob traversal pruning, reserved glob seeds, selected personal ownership, native/mounted parity and all 254 upstream fixture checks. Publish logs and binaries are policy-publish-trimmed.log, policy-publish-native.log, policy-trimmed/ and policy-native/ under the same owned review directory. Builds are bounded and serial; pinned dependency linker diagnostics remain visible with explicit warning qualification. Temporary RID lockfile churn was restored after publishing.

mise run openspec:verify passes all 21 active/spec items, managed checks and both extension tests. Changed-file F# and C# format checks and full-range whitespace checks pass. Main remains 3d5b41c0f839c7b2648403ad6aba5c5644f7dfb5. Current-head CI, developer sign-off, main landing and owned artifact cleanup remain pending.

16.1.6.13 -

Shared configuration delivery evidence

Campaign 1 tracks issue #458. The owned checkout is /Users/damian/.t3/worktrees/morphir-dotnet/t3-722f8d5f, branch t3/add-shared-configuration.

The prepared baseline was c6fcf0cbec80a25d135d0cfffe2c5f61d03514a4, including all six planning snapshots. Main 1c8aa75f63d7ede19cb2ad75b73a6ee8690e4f6f was merged without rewriting history at 26aa85b05df41b83b25f3fc97b19e5c97843babc. The initial checkout was clean. Campaign 1 has no implementation prerequisites.

Checkpoint practice

Commit coherent, owned changes after contracts/tests, passing implementation slices, and documentation/spec updates, and before handoffs or disruptive workspace operations. Identify red checkpoints explicitly. Record validation and remaining work here and in handbacks. Preserve unrelated staged/unstaged work; never stage local docs/superpowers/ artifacts or add AI coauthor trailers.

The parent owns review, CI watching, landing, and campaign checkout/branch cleanup. An open PR or checked task list does not complete this campaign.

Validation journal

  • Initial red: dotnet run --project tests/Morphir.Configuration.Tests -p:BuildInParallel=false -m:1 -- --filter 'Document merge' --sequenced failed with FS0039 for the absent ConfigValue and ValueMerge APIs. No production document behavior had been added. All implementation tasks remain.

  • bb51a21: initial red contract, FS0039 for the absent document APIs.

  • 0fe2abb: additive contracts; first array replacement Expecto case passes. The successful compilation used dotnet run, which forwarded -m:1 to Expecto and failed runner argument parsing. Subsequent checks use dotnet build <project> -m:1 -p:BuildInParallel=false, followed by the built executable with --sequenced for Expecto.

  • 6423ca7 / 294de9f / fe23ee9: profile selector red/green checkpoints.

  • a727a2a: 10 profile cases pass, including raw control nulls and inactive cycles/depth. Ordinary-null overlay case was observed red, then both document merge cases passed. The initial format-order resolver case passes. These are focused results; full source/profile/reader qualification remains in progress.

  • 92c7253: reader checkpoint is explicitly incomplete. Reader build has zero warnings/errors; typed values and extended JSON5 tests pass. Classic normalization was observed red before its implementation.

  • Source resolution red group: capped core build succeeds with zero warnings; 9 focused cases run, 4 pass and 5 fail for runtime overrides, selected-source deduplication, member filtering/defaults, environment mapping, and required selected misses. Member profile-control authority also fails as expected, 10 other profile tests pass. This checkpoint is not PR-ready.

  • 287f1ec: pinned null/source contract checkpoint above. Subsequent capped core build has zero warnings/errors; Resolution 9/9 and Profiles 11/11 pass. Pure environment mapping, selected contribution deduplication, complete ownership filters, default settings, and selected missing-file errors now pass their previously failing cases. Validation, mixed-format warnings, remaining profile bounds, reader qualification, coverage/AOT and final docs remain.

  • c48803f: passing supplied-source implementation checkpoint. Canonical schema validation and mixed-format diagnostics were then observed red and made green, Resolution 13/13. The profile worker’s f06fecb has 33/33 profile cases and the then-current complete core suite 81/81 passing. A follow-up default-priority regression was observed red, expected host IR default 3 but got builtin 4. Its source-order fix and selected-classic fallback cases await the next core run. Reader 36/36 includes official JSON5 syntax checks and pinned corpus hashes. YAML Default profile expansion and full qualification remain underway.

  • 542f129: accepted YAML Default policy, reader normalization and policy docs checkpoint. 93305d9 finishes 39 profile cases. Fresh complete core run passes 88/88 with zero build warnings, including host/default priority and selected classic checkpoint-default regressions that were observed red first.

  • dcedbb3: readers pass 63/63 with zero build warnings, including the two negative-zero cases observed red, official JSON5 conformance, YAML Default, raw legacy loss accounting and lazy profile callback protection.

  • DOTNET_PROCESSOR_COUNT=2 dotnet build Morphir.slnx -m:1 -p:BuildInParallel=false -p:RestoreDisableParallel=true succeeds, zero errors and 20 warnings in unchanged package/generated-code projects. Both configuration suites are wired into the existing Nuke test target using their configured runners, with bounded workers and compiled output reuse. The managed non-CLI smoke executable succeeds.

  • The first complete tooling run used macOS’s default /var/folders/... TMPDIR and failed 78 generation/input-capture cases because those tests reject linked ancestors. CI explicitly uses a physical temporary directory. A rerun with /private/tmp/morphir-shared-config-tests is in progress; no production code was changed for this environment adjustment.

  • OpenSpec progress is 7/10. Remaining: reader trimming/AOT qualification, legacy tooling compatibility, final format/coverage/build evidence. Landing and owned worktree/branch cleanup remain parent-coordinated after PR handback.

Runtime and coverage qualification checkpoint

  • 99aa058 protects integration, seven completed tasks and the full-solution build. The physical-temp tooling rerun passes 245/245. Task 4.1 is complete; implementation progress is 8/10 before final qualification tracking.
  • Fantomas 5.2.4 formats owned F# files; dotnet format formats owned C# files. C# formatting reports unsupported F# projects, so Fantomas is run separately.
  • After formatting, core build has zero warnings and Expecto passes 88/88. dotnet test tests/Morphir.Configuration.Tests/Morphir.Configuration.Tests.fsproj --no-build --no-restore -m:1 --collect:'XPlat Code Coverage' --results-directory /tmp/morphir-configuration-core-coverage also discovers and passes all 88 tests. The configured reader executable passes 63/63 with --coverage --coverage-output-format cobertura --coverage-output /tmp/morphir-configuration-reader-coverage.cobertura.xml.
  • The raw core-run package line rate is 86.82%; the reader-run adapter package rate is 88.26%, including generated regex implementation. Unioning normalized source-line hits from both configured runners gives core 2074/2208 = 93.93% and authored adapter code 434/461 = 94.14%. Generated obj/ code is excluded from that authored-source summary. Reproduce with scripts/configuration/summarize-coverage.py <core-cobertura> <reader-cobertura>.
  • The strict trimmed linker-as-errors publish fails in the pinned CsToml and FSharp.Core packages. Expanded diagnostics include CsToml generic formatter fallback reflection and FSharp.Core structured formatting/reflection plus IL2040 absent-resource removal entries. No warning suppression was added.
  • Actual trimmed and native osx-arm64 executables publish and run using normal warning severity, explicitly -p:ILLinkTreatWarningsAsErrors=false. The native artifact is confirmed as a Mach-O arm64 executable. Its expanded fixture run succeeds with syntax_cases=228, morphir_cases=26, and all three formats, normalization, profile loading and host parity checks. This is runtime evidence with a documented dependency-warning limitation, not a warning-free strict linker claim. See the smoke-host README for exact commands and diagnostics.
  • A final semantic review found a schema minimum check coercing very large exact integers through double. Its focused test is observed red after a successful zero-warning build. This checkpoint retains that incomplete regression before fixing the comparison. Final tests, warning-aware runtime qualification and OpenSpec archive/PR handback remain.

Final implementation validation

  • 1029b94 protects the runtime qualification and observed red integer-bound regression. Integer bounds now compare Int64/BigInteger exactly. Fresh full core runner and VSTest coverage run pass 89/89; reader/BDD coverage run passes 63/63, both with zero build warnings/errors.
  • Final merged authored-source coverage is core 2076/2210 = 93.94% and readers 434/461 = 94.14%. The summaries normalize Cobertura source roots and union hits from the Expecto/VSTest and executable MTP runners. Generated obj/ code is excluded. The generated pure schema is checked for deterministic regeneration.
  • Final trimmed and native hosts both return success with 228 JSON5 syntax checks and 26 pinned Morphir documents. They additionally exercise Default YAML aliases/tags/nulls, all four TOML temporal types, and a 401-digit integer’s schema minimum. The native binary is arm64 Mach-O. Commands explicitly retain normal warning severity; upstream diagnostics and strict linker failure remain visible in the qualification README. No warning suppression or reflection serializer was introduced.
  • openspec validate add-shared-configuration --strict --json passes without issues after splitting long new requirements into focused behavior contracts. No requirement behavior was removed. Repository restore resets publish-only RID/ILCompiler lockfile state, preserving the normal project dependency locks.
  • The typed-host error-parity audit found that pure normalization lacked the text reader’s non-mapping-root diagnostic. Its new focused case was observed red, then the same config.document_shape error was added without altering valid documents. Complete suites now pass core 90/90 and reader 63/63, with zero build warnings. Final runtime/coverage refresh follows this code checkpoint.

Delivery validation receipt

  • Validated implementation checkpoint: d5e4d2f. Complete core 90/90, reader/BDD 63/63 and tooling 245/245 pass. The full solution builds with zero errors and the same 20 package/generated-code warnings in unchanged projects.
  • Full dotnet format Morphir.slnx --no-restore --verify-no-changes --verbosity quiet exits zero. Fantomas checks all owned F# files successfully. The schema generator reproduces its formatted output byte for byte. git diff --check passes.
  • Final configured coverage runs pass 90 and 63 tests. Their source-line union is core 2079/2213 = 93.94% and authored readers 434/461 = 94.14%. Reports are /tmp/morphir-configuration-core-coverage-delivery/9ad9e498-b5be-4d0a-a1e2-02ba9374276c/coverage.cobertura.xml and /tmp/morphir-configuration-reader-coverage-delivery.cobertura.xml.
  • Both final published executables return {"success":true,"formats":3,"json5":true,"normalization":true,"profile_loading":true,"host_parity":true,"syntax_cases":228,"morphir_cases":26}. Publish logs are /tmp/morphir-config-trimmed-delivery.log and /tmp/morphir-config-native-delivery.log; binaries are under /tmp/morphir-config-trimmed and /tmp/morphir-config-native. Warning severity is explicitly normal for runtime qualification. Strict linker-as-errors still fails in pinned dependencies; no diagnostic suppression hides that limitation.
  • All 10 tracked implementation tasks are complete and OpenSpec apply reports all_done. Strict change validation passes. This does not declare campaign completion: PR review/checks, main landing and owned worktree/local/remote branch cleanup remain parent responsibilities.

Specification handback

The completed implementation change is archived at openspec/changes/archive/2026-11-02-add-shared-configuration/. Main specifications are openspec/specs/configuration-documents/spec.md and openspec/specs/configuration-resolution/spec.md. All 22 added requirements and their scenarios were compared with the delta specs; new Purpose sections were copied verbatim. Strict validation passes all 9 main specs and all 7 remaining active changes, 16/16 items with no issues.

The other five campaign plans remain active with zero checked implementation tasks. Only their shared-configuration prerequisite links were updated where needed. The original six-plan snapshot remains in inherited commit c6fcf0c and this PR’s history. Do not start dependent implementation until this PR lands.

Parent handback must record the created PR URL, actual head SHA and base SHA, then own review/check watching, merge verification and separate worktree/local/remote branch cleanup receipts. No PR watch, branch deletion, worktree removal, force push, admin bypass, release or deployment was performed by this implementation worker. Reproducible build/coverage logs and published smoke artifacts under /tmp/morphir-config-* are retained for parent inspection and later cleanup.

Current-base reconciliation

Before PR creation, main advanced to 890aacb61df601f2876112a2cdd4d9d6f5757ca6 with documentation-only C# generation/SDK planning, PR #459. It was merged at 31598987b0edc4d5db27c3237e13df9caf4124eb. The merge changes no src, tests, build, solution, dependency or SDK/toolchain inputs, so the recorded runtime validation remains compatible. Strict OpenSpec validation on the reconciled base passes 21/21 items: 12 active changes and 9 main specs. All unrelated plans are preserved. Review/landing/cleanup remain outstanding at this handback checkpoint.

Parent review checkpoint

The full PR diff whitespace check identified literal CR/CRLF bytes in the pinned JSON5 newline grammar fixtures. A narrow root attribute disables whitespace checking for those raw newline fixtures; their original bytes and hashes remain unchanged. An extra trailing blank line was removed from the shared reader BDD feature. These review changes do not change production behavior or dependency inputs. Full-range git diff --check passes with the fixture attribute applied.

Windows CI on the earlier head exposed checkout normalization of the three hash-pinned configuration corpus/schema snapshots. Git’s core.autocrlf=true checkout filter reproduced all three reported hash mismatches. The configuration fixture tree now uses -text, matching the existing Ion-fixture policy; the same checkout-filter check preserves every expected snapshot hash afterward. CI must confirm this fix on Windows before landing.

Parent semantic review fixes

CI passed all checks on 51e9eca, including all three operating systems. Parent review then addressed six distinct findings before landing:

  • 0037d66: a failing resolver test confirmed that environment JSON secret references retained stale lower object fields. Environment values now use the shared path-aware secret normalization and retain their original raw tree. A second red test showed logging.file being treated as a credential, losing the file when a later layer added a logging level. The root and the schema’s logging container keep ordinary map semantics.
  • 6ccff70: three culture regressions failed for Thai calendar years, Finnish time separators and an early date outside the Saudi calendar’s range. Typed TOML temporal getters now use explicit invariant ISO formatting, keeping local date-times without an offset and preserving explicit offsets. Tests exercise the pinned parser’s supported microsecond precision.
  • c72bab7: red tests reproduced a missing validation error for a null-only object replacing an integer, and an error attributed to a lower empty object after a higher source populated it. Empty replacement objects now retain provenance; populated maps discard obsolete empty-object leaf origins.
  • c795935: a red test demonstrated the missing pinned-schema string-pattern rule. The generator now emits pattern rules and validation evaluates them with culture-invariant, nonbacktracking regular expressions. Leading/trailing whitespace and whitespace-only Elm extensions fail at the supplying source.

The complete core runner passes 95/95. The reader/BDD runner passes 66/66, including the culture regressions. Both projects build without warnings. The trimmed and native osx-arm64 smoke hosts reran 228 JSON5 syntax checks and 26 Morphir documents, with new culture and schema-pattern checks. Both passed. Publish commands match the smoke README, with outputs under /tmp/morphir-config-review-trimmed and /tmp/morphir-config-review-native; the native log is /tmp/morphir-config-review-native.log. Existing pinned dependency diagnostics remain visible under the previously documented warning qualification. No dependency versions, fixture bytes or lockfiles changed.

Fresh core VSTest/Coverlet and executable reader coverage runs both pass. Their union reports core 2134/2267 authored lines, 94.13%, and readers 441/468, 94.23%. The reports are /tmp/morphir-config-review-core-coverage/ee02bb36-e337-4426-b669-537601fa7447/coverage.cobertura.xml and /tmp/morphir-config-review-reader-coverage.cobertura.xml, processed by the existing scripts/configuration/summarize-coverage.py tool. Changed-file Fantomas checks and C# whitespace formatting pass; full-range and working-diff whitespace checks pass. New-head CI, landing and owned checkout/branch cleanup remain outstanding.

Current-main dependency requalification

All eight CI checks passed on 3684497; all inline review threads are resolved. The user explicitly authorized merging with admin bypass. Before executing the merge, a fresh main-ref check found 125bfcbbd97f343c39606d923ad17a6b3e2ecaa4, ahead of the previously validated base. Its changes include SDK conformance fixtures/tests and dependency patches, including CsToml 1.8.3 to 1.8.6.

bb4fb95 integrates current main without conflicts. Both the configuration runners and the SDK conformance runner remain in the combined Nuke test target. b210343 updates only the new configuration projects’ affected lockfiles and reader version documentation to match main’s CsToml 1.8.6 and System.IO.Hashing 10.0.11. No dependency version from main is reverted.

Requalification on that integrated tree:

  • Full Morphir.slnx build passes, zero errors and the same 20 warnings in existing package/generated-code projects. It uses two logical processors, single-node MSBuild and disabled parallel restore/build.
  • Configured runners pass core 95/95, reader/BDD 66/66, SDK conformance 27/27, and tooling 245/245. Tooling uses /private/tmp/morphir-shared-config-tests for its physical temporary directory and one test worker.
  • mise run openspec:verify passes the managed schema/snapshot checks, strict validation of all 21 active/spec items, and both extension tests.
  • Strict trimmed publishing still fails on pinned dependency diagnostics. The warning-qualified trimmed and native hosts both pass 228 JSON5 syntax checks, 26 Morphir documents, and the host’s culture, schema, normalization, lazy-profile and in-memory parity checks with CsToml 1.8.6. The serializer source at the NuGet repository commit retains its closed TomlDocument branch before generic formatter resolution; the smoke README links that revision.

Reproducible logs are /tmp/morphir-config-main-refresh-build.log, /tmp/morphir-config-main-refresh-tooling.log, /tmp/morphir-config-main-strict.log, /tmp/morphir-config-main-trimmed.log, and /tmp/morphir-config-main-native.log. Published hosts are /tmp/morphir-config-main-trimmed and /tmp/morphir-config-main-native. Publish-generated RID/ILCompiler lock changes were removed after qualification, retaining the committed dependency updates.

Current-head CI, the authorized admin merge, landed-commit verification and owned artifact cleanup remain outstanding.

16.1.6.14 -

Workspace migration delivery journal

Tracking issue: #479. Change: add-workspace-migration, campaign 6 of six.

Ownership and baseline

Owned checkout /Users/damian/.t3/worktrees/morphir-dotnet/t3-f370bf10, branch t3/workspace-migration, initially clean at exact required main baseline e3b9a053120a49e94c70c30cd4f62ef783672c64. GitHub main agrees. Prerequisite PR #477 is merged at that same SHA, and its receipt confirms review/checks and owned cleanup. The real open tracking issue was created and read before coding.

OpenSpec list/status/instructions apply returned schema html-design, ready, 0/9 tasks. Full returned proposal/design/spec/tasks and layered configuration PRD, project/config editing journals and shared APIs were read. Only this change will be synchronized/archived when all implementation tasks are genuinely complete. Independent active plans remain untouched.

Parent owns independent final review, CI watch, authorized landing and checkout/branch/artifact cleanup. This implementation worker does not watch, merge or delete checkout/branches. The campaign remains incomplete until parent gates finish.

Work and evidence ownership

Lead owns bounded inventory, aggregate composition/verification, integration and qualification. Two Codex helpers own shared transaction/recovery and CLI/report/schema/subprocess tests respectively. Helpers preserve one another’s changes. One heavy restore/build/test/publish at a time, DOTNET_PROCESSOR_COUNT=2, serial MSBuild/restore, sequenced Expecto and TUnit --maximum-parallel-tests 1.

All owned external artifacts are under /private/tmp/morphir-workspace-migration/, including physical tmp/, transactions/, cli/, build/runner/coverage/publication logs and manifests. Global caches/tools are not changed. Process-local PowerShell 7.5.3 is used for F# SDK tests. Unresolved recovery journals/backups are preserved and inventoried.

Checkpoints

Setup completed before runtime changes. Proposal now links actual issue #479. Task 1.1 was updated at returned line 8 and apply progress rechecked.

Inventory and planning checkpoints

  • inventory-red-build.log proves the aggregate API was absent before production. Initial BDD and API cases preceded implementation. inventory-io-full-01.log passes 323/323 IO/TUnit/Reqnroll cases, including three workspace BDD cases.
  • planning-red-run.log passes 10 and fails four of 14 focused cases, reproducing repeated-inventory classification, non-adoption setting drift, glob reopening skipped/nested directories and filtered-root identity. planning-green-run.log passes all 14 after fixing those behaviors through shared discovery and mapping.
  • boundaries-red-run.log passes four and fails four boundary cases. These expose retained classic root context, configured output skipping, invalid defaults and paths containing glob syntax. recovery-preview-red-run.log separately fails pending-journal preview before read-only recovery inspection is added. boundaries-green-run.log passes all 23 focused planner/application cases with clean compilation, including source/target/listing/topology drift and repeat source-byte/target-byte/timestamp checks. No generated output is moved.
  • Early inventory build attempts while recovery production was incomplete and a zero-test filter run are retained as unsuccessful attempts, not qualification. A lightweight FSI probe had an FSharp.Core load mismatch; configured executables provide correctness evidence instead.
  • Tasks 2.1 and 2.2 were checked at fresh CLI-returned locations and progress confirmed after each update. Aggregate equivalence/adoption qualification and application/recovery qualification remain active.

Coordinated application and recovery

  • transactions/batch-red-run.log fails both first-stage ordering cases before production; later staging and recovery runs prove every candidate and backup exists before the first replacement. Complete preflight blocks a bad member, stale source/target/listing/alternate primary and overlapping writers.
  • transactions/recovery-red-build.log and inventory-build-01.log prove missing explicit inspection/recovery APIs. transactions/boundary-red-run.log reproduces read-only unrelated-directory staging and lost common-ancestor cleanup guards.
  • transactions/capability-red-run.log proves a legacy custom host could write without durable enumeration. Application now returns a typed capability failure before artifacts unless a host explicitly supplies durable support.
  • transactions/evidence-red-run.log reproduces null/overflow journal parser exceptions. Recovery now reports invalid metadata without changing it. Stable artifact names are tied to target, role and owner. A mounted serialized-state host proves recovery is not process-memory-only; tampered backups/user changes preserve evidence and block automatic restoration.
  • transactions/final-precondition-red-run-02.log reproduces source drift after the last install. Final rechecking now detects it and attempts rollback. transactions/parent-creation-red-run.log reproduces a process exit after mkdir but before the host returned its ownership receipt. A durable initial journal now precedes mkdir; uncertain directories remain explicit and require reviewed retention, never inferred cleanup ownership.
  • transactions/last-windows-green-run-02.log passes 87 focused cases. Seven actual process-interruption windows and separate-process pending-write refusal pass. planner-final-build-02.log is clean, and planner-final-io-02.log passes the complete current-source IO runner, 371/371, after the lead’s later fixes.
  • Success and proven rollback clean owned artifacts. Explicit recovery resolves test-owned native faults. Retained malformed parser samples are synthetic; a pre-fix mkdir-crash fixture retains generated empty directories and child logs as review evidence, without a recoverable target or journal. Those samples are preserved for parent cleanup. No real unresolved native journal/backup was found in the helper’s final inventory.
  • Native coordination is machine-local, and file replacement is per-file atomic. No cross-file crash-atomicity or distributed-lock guarantee is claimed.

Common-context verification and source policy

transactions/lead-declared-controls-red.log passes one and fails one declared control test. transactions/lead-layout-red.log fails both mixed-layout cases. The aggregate now compares declared controls with activation disabled through the same shared control policy as single-project verification. It supplies an explicit existing supported TOML target to the shared planner, preserving owner anchors and all existing target/ambiguity guards. Both root and member layouts pass.

source-policy-red-run.log reproduces a normalized legacy JSON5 primary being misreported as already migrated. Inventory now sends every normalized legacy candidate to the project planner, which retains its established .json classic source contract and returns migration.classic_required for .json5 primaries. Full JSON5 syntax in supported morphir.json remains accepted; no reader policy is changed or loosened. Canonical JSON5 projects remain valid canonical projects.

The first final planner build tried to call an internal F# helper from C# and failed compilation. Its subsequently invoked stale-DLL tests are not a current qualification gate. planner-final-build-02.log is the successful rebuilt source, followed by planner-final-io-02.log, 371/371 current tests. Task 2.3 was marked only after that qualification, at the refreshed source location.

CLI adapter and subprocess contracts

The actual baseline CLI and tooling test assembly were built before CLI runtime implementation. cli/cli-red-run-02.log contains 27 intended missing-command failures; two early alias-test assertions were corrected to the existing generic usage contract and are not claimed behavioral evidence. The first integrated 29-case run passed 27 and exposed a missing-filter token being consumed as text and an unstructured workspace host-initialization crash. Both were reproduced and fixed. Its mutable log was later overwritten by a script; terminal evidence and cli/parse-probe.log remain, so that overwritten file is not claimed as the original failure receipt. Later ordinal checkpoint receipts are immutable.

cli/cli-green-02-run.log passes 29/29 after actual CLI rebuilding. An isolated existing-DLL probe then reproduces a pending-journal dry-run incorrectly returning applicable preview. cli/pending-cli-red.log and the associated JSON receipt preserve that failure, followed by explicit shared rollback. The final adapter surfaces pending recovery, uncertain directories and preserved directories through safe JSON and human outcomes. cli/cli-green-03-run.log passes all 31 subprocess BDD cases after rebuilding the actual CLI against final shared sources. Every operation report validates against the checked-in schema using a fresh registry.

The cases include current/supplied bounded roots, repeated include/exclude, root/virtual/existing workspaces, no adoption/no-op, loss blockers, raw nested secret redaction, depth 120, parse/initialization failures, source preservation, no config migrate alias, durable pending recovery and a real process interrupted after mkdir. Explicit recovery resolves every real CLI fixture. The actual CLI build retains two existing package warnings; tooling retains three existing package/generated warnings. cli/format-03.log passes own C# formatter verification. cli/final-receipt.json and cli/final-source-sha256.json record exact commands, counts and source identity. Task 4.1 was checked at its refreshed returned location.

Live artifact ownership review

During final qualification, lead review identified two unverified live-application ownership boundaries. Stage return paths were registered as owned without checking their deterministic target/role identity, and live cleanup did not first check whether a backup/journal had changed. Eight new regressions are written before fixes. Tasks 3.2 and 3.3 were explicitly reopened at fresh returned checkbox locations; earlier passing receipts are source checkpoints, not final qualification of these guards. Broad runner evidence is retained as an initial checkpoint while the new cases are reproduced and resolved.

The initial broad run completed all 17 configured executable runners successfully before the live-ownership fixes. Its receipts are preserved in validation-initial/; they are not substituted for final-source qualification. Counts include core 177, IO 371, tooling 433 and Microsoft 22. The initial full solution build has 11 existing dependency/generated warnings and no errors. C# full formatter verification and changed F# Fantomas checks pass. The final ownership regression set reproduces all ten failures in transactions/live-ownership-red-run.log, with a clean preceding build.

Coverage-driven public API qualification

Fresh managed non-CLI qualification passes 28 workspace checks, 32 project migration checks, 44 editing checks, 228 JSON5 grammar cases and 26 pinned Morphir documents. The compiled host exercises batch preview/application, safe generated report serialization, source retention, output retention, byte/timestamp repeats, drift and explicit recovery. qualification-first-summary.json records exact commands and exits; all six format/build/smoke/collector gates pass.

The first current-source coverage union is core 4188/4345, 96.39%, and IO 2367/2459, 96.26%. IO is below prerequisite 97.15%, so final qualification remains open. Public API boundary tests were added for bounded invalid inputs, mounted listing/read/drift failures, native staging/parent failures, explicit recovery faults/capabilities/options, encodings and full JSON5 escaped values/opaque arrays.

That work also reproduced a real multi-format no-op defect: an existing declared workspace without root TOML could stage an empty primary despite Changed=false. format-noop-red-run.log fails all six YAML/YML/JSON/JSON5/supplemental/hidden variants before the fix. The aggregate now overlays a root file only when it has workspace edits or an actual root conversion. format-noop-green-run.log passes all six with unchanged source bytes and no new primary. Task 2.2 was explicitly reopened for this finding before the minimal production fix.

Final discovery and composition corrections

discovery-verification/actual-discovery-probe.json records the initial defect: application could discover four members after a preview verified only one through a sanitized inventory tree. The native discovery regression suite first fails three of four cases, then passes all four. Verification now uses actual shared discovery listings, retains filtered existing membership and rejects declared globs that cross a hard dependency/output/nested-workspace boundary. It verifies both retained-classic and source-excluded views. No generic discovery semantics or existing member declarations are changed to make verification pass.

root-composition-red-run.log reproduces a root classic manifest’s original membership being rejected against adoption’s own new members. Shared component planning now maps against the original target, validates explicit proposed membership against discovery, and composes only the reviewed membership edit. A fabricated original target and undeclared membership are rejected. A separate red case proves component plans must not independently apply hypothetical workspace context; components are now read-only and aggregate application owns all writes. Existing target membership conflicts remain blockers.

root-base-run.log passes all seven composition cases after actual rebuilding, including custom output roots and workspace.root='sub' with a root source path. The common model uses canonical ownership controls without rendering temporary configuration or weakening raw unsupported-value accounting. Every member uses the same base workspace; ordinary source paths/settings retain the shared single-project verifier. root-composition-green-io.log passes the preceding 428-case full IO checkpoint; final qualification below supersedes it.

Live artifact ownership regressions now pass. Stage and directory receipts must match predeclared deterministic ownership; cleanup validates every owned artifact before removing any. Altered backups/journals and unverified foreign stage paths retain evidence and durable overlap guards. Inspection exposes unresolved paths after process-local result loss. Deliberately unresolved fault fixtures remain under the owned external artifact root for parent inspection; they are not silently cleaned or represented as successful recovery.

Final runtime qualification

Runtime checkpoint: de44fa5 (fix: verify aggregate context and artifact ownership), based on exact main e3b9a053120a49e94c70c30cd4f62ef783672c64. qualified-source-checkpoint.json records hashes of every changed file. Subsequent delivery edits are documentation/specification only. All following commands run from the owned checkout, one heavy operation at a time, with DOTNET_PROCESSOR_COUNT=2, BuildInParallel=false, RestoreDisableParallel=true and TMPDIR, TEMP, TMP set to /private/tmp/morphir-workspace-migration/tmp.

  • Locked solution restore: dotnet restore Morphir.slnx --locked-mode -m:1 -p:BuildInParallel=false -p:RestoreDisableParallel=true.
  • Final build/analyzers: dotnet build Morphir.slnx --no-restore -m:1 -p:BuildInParallel=false; zero errors, ten existing package/generated warnings. Actual CLI rebuilt before subprocess qualification.
  • Changed C# formatter: dotnet format Morphir.slnx --no-restore --include <all changed C# paths> --verbosity quiet; exact expanded argv is in qualification-first-summary.json. Full formatter verification uses dotnet format Morphir.slnx --no-restore --verify-no-changes --verbosity quiet.
  • F# formatting: dotnet fantomas --check src/Morphir.Configuration/Migration.fs src/Morphir.Configuration/ProjectDiscovery.fs.
  • Managed host: dotnet tests/Morphir.Configuration.IO.Smoke/bin/Debug/net10.0/Morphir.Configuration.IO.Smoke.dll --fixtures tests/Fixtures/Configuration.

The final configured executable sweep passes all 17 runners, 3,887 tests and smoke checks. Each invocation is dotnet tests/<project>/bin/Debug/net10.0/<project>.dll with the flag below. The process-local PATH adds campaign bin/dotnet for serial nested builds and existing PowerShell 7.5.3 for F# SDK package tests; DOTNET_HOST_PATH points to that campaign wrapper. Global tools/caches are untouched.

ProjectRunner flagsPassed
Morphir.CodeGeneration.FSharp.Tests--sequenced333
Morphir.Configuration.IO.Tests--maximum-parallel-tests 1429
Morphir.Configuration.Inspection.Smokenone71
Morphir.Configuration.Microsoft.Tests--maximum-parallel-tests 122
Morphir.Configuration.Tests--sequenced177
Morphir.Core.Tests--maximum-parallel-tests 1237
Morphir.IR.Pipeline.Plugins.Tests--sequenced141
Morphir.IR.Pipeline.Tests--sequenced299
Morphir.Internal.CodeGeneration.Tests--sequenced25
Morphir.Ion.Tests--sequenced463
Morphir.Models.Tests--sequenced791
Morphir.Myriad.Integration.Tests--maximum-parallel-tests 112
Morphir.Myriad.Tests--sequenced4
Morphir.SDK.CSharp.Tests--maximum-parallel-tests 120
Morphir.SDK.Conformance.Tests--maximum-parallel-tests 127
Morphir.SDK.Tests--sequenced403
Morphir.Tooling.Tests--maximum-parallel-tests 1433

validation/final-receipt.json preserves expanded commands, durations and counts; individual logs preserve actual runner discovery. IO includes shared/project/ workspace BDD, full aggregate fault/recovery/ownership and separate-process interruption/overlap tests. Tooling includes 31 real workspace subprocess BDD scenarios and fresh-registry JSON-schema validation. Models/SDK runners retain codec/roundtrip contracts. Plain dotnet test is not substituted for these runners.

Coverage commands:

dotnet tests/Morphir.Configuration.IO.Tests/bin/Debug/net10.0/Morphir.Configuration.IO.Tests.dll \
  --maximum-parallel-tests 1 --coverage --coverage-output-format cobertura \
  --coverage-output /private/tmp/morphir-workspace-migration/io.cobertura.xml
dotnet test tests/Morphir.Configuration.Tests/Morphir.Configuration.Tests.fsproj \
  --no-build --no-restore -m:1 -p:BuildInParallel=false \
  --collect:"XPlat Code Coverage" \
  --results-directory /private/tmp/morphir-workspace-migration/core-coverage
/usr/bin/python3 scripts/configuration/summarize-coverage.py \
  /private/tmp/morphir-workspace-migration/core-coverage/*/coverage.cobertura.xml \
  /private/tmp/morphir-workspace-migration/io.cobertura.xml

Final source coverage: core 4,188/4,345 (96.39%); IO 2,495/2,561 (97.42%). Both exceed prerequisite source checkpoints. The core collector is supplementary; 177 sequenced Expecto tests establish actual test execution. No source exclusions or warning suppressions were added to meet qualification.

Managed, trimmed and native non-CLI batch hosts each pass 38 workspace checks, 32 project migration checks, 44 editing checks (9 editor, 22 shared, 13 transaction), 228 official JSON5 syntax cases and 26 pinned Morphir documents. They exercise read-only/native preview, aggregate application, no-op bytes/timestamps, source and generated-output retention, drift, redacted generated serialization, explicit journal recovery and uncertain-directory preservation. Native output is a Mach-O 64-bit arm64 executable. Exact publish argv and exits are in publish/summary.json:

dotnet publish tests/Morphir.Configuration.IO.Smoke/Morphir.Configuration.IO.Smoke.csproj \
  -c Release -r osx-arm64 -m:1 -p:BuildInParallel=false \
  -p:RestoreDisableParallel=true -p:PublishTrimmed=<false|true|true> \
  -p:PublishAot=<false|false|true> -p:ILLinkTreatWarningsAsErrors=false \
  -p:TrimmerSingleWarn=false -p:JsonSerializerIsReflectionEnabledByDefault=false \
  -o /private/tmp/morphir-workspace-migration/publish/<managed|trimmed|native>
/private/tmp/morphir-workspace-migration/publish/<mode>/Morphir.Configuration.IO.Smoke \
  --fixtures /Users/damian/.t3/worktrees/morphir-dotnet/t3-f370bf10/tests/Fixtures/Configuration

The three modes run sequentially with the corresponding values above. Managed publish has zero warning lines; trimmed retains 44 and native retains 47 pinned CsToml/FSharp.Core analysis-warning lines. These remain visible upstream limitations, not a strict warning-free trimming claim. No suppression or package update was introduced. publish-locks/restored.json proves all 45 lockfiles were restored byte-for-byte after publishing. Reports and stderr are retained separately.

Specification sync and handoff gates

Full formatter verification and final locked restore pass. OpenSpec apply returned all_done, 9/9, before archive. The current specs-rule snapshot was read before writing the new main spec. Only add-workspace-migration was synchronized and archived at openspec/changes/archive/2026-11-03-add-workspace-migration/. openspec/specs/workspace-migration/spec.md preserves the approved Purpose and all 11 requirement bodies/14 scenarios byte-for-byte under canonical main-spec headings. archive-verification.json verifies exact content, nine checked tasks, retained .openspec.yaml, balanced HTML and valid local design/proposal links. The archive’s prerequisite and PRD links are adjusted for archive depth.

MISE_TRUSTED_CONFIG_PATHS="$PWD" mise run openspec:verify passes before and after archive: managed html-design snapshot/schema, all 22 current change/spec items, and both extension tests. openspec validate --specs --strict --json passes 18/18 and openspec validate --archived --strict --json passes 11/11. No archived path is passed as a direct CLI item name. Independent active plans are unchanged against the required baseline. NOTICE and contribution policy remain satisfied; no AI coauthor trailers or local docs/superpowers/ artifacts are staged.

The implementation is locally qualified and archived, but this sixth campaign is incomplete. Parent owns independent final review, current-head CI watching, authorized landing and verified owned checkout/local-branch/remote-branch/artifact cleanup. This worker opens one focused PR with issue #479 and exact head/base, then hands back without watching, merging, deleting sources/branches/checkouts, releasing or deploying. PR head and forge state belong in the PR/issue handoff receipt; this committed runtime checkpoint is not a claimed review/CI approval.

Owned external artifact and recovery inventory

All external artifacts remain beneath /private/tmp/morphir-workspace-migration/. artifact-inventory.json lists every owned file and artifact-inventory-summary.json lists retained recovery paths without configuration values. At final qualification the inventory contains 2,739 files (320,872,941 bytes), excluding the inventory’s own output files; final PR/receipt files added afterward are also owned beneath that same root. Major groups are:

  • tmp/: physical test/subprocess temporary roots and deliberately retained fault fixtures, including injected user-change and ownership evidence.
  • transactions/, cli/, discovery-verification/: immutable red/green/probe, interruption and explicit-recovery receipts. Two deliberately malformed parser samples have no real destination operation.
  • validation/: final 17-runner logs/expanded commands/counts; validation-initial/ is explicitly an earlier checkpoint.
  • publish/: three published hosts, build logs, separate JSON results/stderr; publish-locks/: 45 original lockfiles and byte-identical restoration proof.
  • Coverage XMLs/summary, format/build/restore/OpenSpec receipts, source hashes and campaign-local orchestration scripts/wrapper. Checkout bin/obj/TestResults are reproducible local build artifacts, not new external cleanup roots.

There are 114 retained .morphir-edit-* evidence files across 37 containing directories: 112 from generated fault fixtures and two synthetic malformed parser samples. These are intentionally unresolved/altered test evidence, not production operations. The exact paths and transaction IDs are in the inventory. Earlier helper receipts reporting no unresolved journals describe their earlier checkpoint; the later ownership/cleanup regressions intentionally retain new evidence. No file was removed or manufactured to improve the cleanup claim. Preserve these journals/backups and their surrounding user-change fixtures until explicitly inspected/recovered or dispositioned by the parent. Owned cleanup therefore remains an explicit parent obligation. Global caches/tools and unrelated campaign artifacts are excluded from ownership.

Current-main interaction at PR preparation

GitHub main advanced during qualification to 0542145a4bcc0edf9c9dd2fb3122df36c73e80d0 through independent C# generation PRs #478, #480 and #482. The owned branch retains the explicitly required starting baseline e3b9a053120a49e94c70c30cd4f62ef783672c64; PR #477 remains its merged prerequisite. No unrelated generation work is copied or modified in this branch. The intervening production changes add a separate generation backend/reference and model package-release notes; they do not change configuration/project/workspace services. README/troubleshooting edits are in different sections. git merge-tree --write-tree origin/main HEAD succeeds without conflicts (integration tree a866f8115332651717e14fe1b0a7ec4b8db3e460 at the documentation checkpoint). This is merge compatibility evidence, not execution of a merged checkout. The 17-runner/runtime receipts qualify this branch at its required baseline; parent-owned current-base PR CI remains necessary for the new backend and additional independent suites. The PR records the actual newer base separately.

Parent policy review follow-up

Independent review of ff5347f identified three concrete defects: existing workspaces bypassed original-value checks for newly adopted projects, supplied exclusions did not prevent root conversion, and new members were reported with existing workspace ownership/output roots before adoption. All five new public API cases fail in parent-review/policy-red-02.log. An earlier malformed filter selected zero tests and is not red evidence. All three actual CLI regressions fail against the original executable in parent-review/policy-cli-red.log.

The shared coordinator now checks each newly adopted project against its original non-adoption settings in both new and existing workspaces. Root exclusions apply to conversion selection while root identity and necessary workspace composition remain. Adoption effects use actual prior membership to distinguish retained and standalone contexts. The native smoke includes these paths.

The first full IO run passed 433 of 434 cases and exposed an older success fixture relying on the newly rejected silent backend change. Its new project now explicitly authors the matching backend, retaining a valid successful preservation case. Its member-local profile activation declaration was also removed so that fixture does not rely on control metadata changing authority through adoption. The rebuilt CLI passes the three new cases. The refreshed full IO runner passes 434 cases. Final full-suite, coverage, runtime, CI and independent transaction-review results remain delivery gates.

All 17 configured executable runners pass again after the parent policy fixes. The full IO runner now passes 434 cases and tooling passes 436, including all three new real CLI regressions; all other runner counts remain as above. validation/summary.json and the refreshed individual runner logs retain exact commands, duration and exits for this current-source checkpoint. The full solution build passes with nine existing warnings and zero errors, and changed C# files passed formatter processing before that build.

Final policy qualification after 19504a8 passes full formatter verification, OpenSpec/extension verification, fresh IO coverage and all three published host modes. The unchanged F# source maps reuse their core collector with the fresh IO report: core 4188/4345, 96.39%; IO 2493/2559, 97.42%, recorded in parent-review/policy-coverage-summary.json. Managed, trimmed and native hosts each pass 42 workspace checks, 32 project migration checks, 44 editing checks, 228 JSON5 cases and 26 Morphir fixtures. Published stderr is empty; trimmed and native dependency warning counts remain 44/47 with the documented explicit warning-severity qualification. Lockfiles are restored byte-for-byte. Exact commands and exits are in parent-review/policy-qualification-summary.json and the refreshed publish summary. Original retained recovery evidence is untouched; new parent validation fault-fixture evidence also remains for final disposition. Independent transaction/recovery review, current-head CI, landing and audited cleanup remain outstanding campaign gates.

Ancestor recovery discovery race

Linux CI on ff5347f fails the valid empty-workspace no-op CLI scenario with config.edit_io. Its generic renderer does not expose the original exception, so the retained CI log alone cannot identify the exact failing filesystem call. A focused concurrent probe then reproduces a relevant native recovery race: FindRecoveryJournals fingerprints every ancestor directory through Observe, and an unrelated sibling disappearing during that fingerprint throws FileNotFoundException. parent-review/recovery-race-red.log records this failure before production changes.

Journal discovery now checks directory kind and refuses linked ancestors without fingerprinting unrelated contents. Source/target/listing observation and stale plan guards retain their original strict behavior. The focused regression scans while unrelated files churn; a companion test preserves linked-ancestor refusal. Both pass in parent-review/recovery-race-green.log. The full refreshed IO runner passes 436 cases. Current-head Linux CI still qualifies the original CI symptom.

All 17 runners pass again after the recovery scan correction: IO 436, tooling 436, core configuration 177 and Microsoft 22; the other counts are unchanged. The full solution build and changed-file formatter pass. The refreshed full runner logs and summary retain current-source execution evidence. This is a verified native discovery race fix; current-head CI remains required before claiming the Linux no-op failure resolved on the runner.

Final qualification after 56eb44a passes full formatter, OpenSpec/extension, fresh IO coverage and all managed/trimmed/native runtime hosts. Core coverage remains 4188/4345, 96.39%; IO is 2506/2573, 97.4%. Each published mode passes 42 workspace, 32 project migration, 44 editing, 228 JSON5 and 26 Morphir checks with empty stderr. Dependency warning counts remain 44/47 for trimmed/native and zero for managed; lockfiles are restored exactly. The latest full rebuild reports eleven existing warnings and zero errors. parent-review/recovery-race-qualification-summary.json, coverage summary and publish summary retain exact commands and results. Current-head Linux CI and independent transaction/recovery review remain open gates.

Independent transaction review follow-up

Independent review of ff5347f reports three P2 failures with retained probes: a durably created committed witness could be followed by rollback, a rejected persisted journal could remain after a false complete-recovery result, and a host observation failure inside error handling could escape without a typed outcome. parent-review/receipt-red.log reproduces all six new cases before production changes, including missing/persisted commit witnesses, rejected journal/stage receipts and disconnection after stage/commit persistence.

Application now validates a possibly persisted committed witness before choosing rollback. A valid witness keeps candidate bytes and backups for explicit completion; an absent witness permits rollback. Explicit recovery preserves rejected transaction-shaped artifacts and reports recovery required while they remain. The failure-handler fallback returns known or possible evidence and unresolved targets without further storage observations or blind rollback. All six regressions pass in parent-review/receipt-green.log; the full IO suite passes 442 cases. Published smoke coverage adds these commit/receipt cases.

The independent read-only audit classified the original 114 retained files across 37 directories as generated test evidence: 28 target-drift, 28 backup-drift, 35 committed-cleanup, 14 partial-journal ownership, seven partial-journal cleanup and two malformed-parser samples. No external/non-fixture references, links or unique user data were found. The disposition inventory records paths, hashes, provenance and inspection results. Original files remain untouched; parent-owned fixture cleanup is authorized after the final review/landing gates. Additional parent regression fixtures are retained separately under the same owned root.

All 17 configured runners pass after the receipt/recovery corrections: IO 442, tooling 436, core 177 and Microsoft 22; other counts are unchanged. The rebuilt solution passes with eleven existing warnings and zero errors. Changed source files passed formatting before the rebuild. The original disposition inventory was rechecked after parent validation: all 149 recorded artifact/adjacent-file hashes still match, recorded in parent-review/original-evidence-hash-check.json. Both independent review scopes are complete; their six findings are addressed. Final coverage, published hosts and current-head CI remain qualification gates.

Final receipt/recovery qualification after ff495a7 passes full formatter, OpenSpec/extension, fresh IO coverage and every published host mode. Core source coverage is 4188/4345, 96.39%; IO is 2537/2606, 97.35%, still above the merged prerequisite’s 97.15%. No exclusions were added. Managed, trimmed and native hosts each pass 59 workspace/recovery checks, 32 project migration checks, 44 editing checks, 228 JSON5 cases and 26 Morphir fixtures. Published stderr is empty. Managed has no warnings; trimmed/native retain the same 44/47 dependency warnings under explicit normal-warning qualification. Lockfiles are restored byte-for-byte. Exact commands and outcomes are in parent-review/receipt-qualification-summary.json, its coverage summary and the publish summary. Both independent reviews are complete; all six findings plus the independently reproduced ancestor-enumeration race are addressed and tested. Current-head CI, landing and audited owned cleanup remain campaign gates.

Windows fixture path correction

Development run 41747846560 on 9590b60 reports five Windows-only fixture failures. The hidden-layout scenario moved the source successfully but failed to remove its old dictionary entry because the original relative path contained forward slashes and the removal used native separators. Four transaction tests correctly retained tampered artifacts, then compared native enumerated paths against the public API’s normalized paths. The captured failing log is parent-review/ci-9590b60.clean.log.

The fixtures now normalize source dictionary keys and enumerated artifact paths before comparing identities. Production behavior is unchanged. The full solution rebuild passes with eleven existing warnings and zero errors. All 17 local runners pass again with 3,903 tests/checks, including IO 442 and tooling 436. Production source and published-host evidence remain unchanged; current-head Windows CI remains the platform qualification gate. Formatting and OpenSpec/extension verification also pass, recorded in parent-review/windows-fixture-postchecks.json. The expanded read-only fixture audit accounts for 326 retained recovery files after repeated validation, with no references outside their individual fixtures, symlinks or unknown adjacent file contents. The original 114-file audit remains intact. Explicit fixture disposition and removal remain deferred until verified landing.

16.1.7 -

Issue #240: Create Elm to F# Guru Skill - Enhanced Edition

Enhancement of: Issue #240
Enhancement based on: Issue #253 - Unified Cross-Agent AI Skill Framework Architecture
Related Issues: #254, #255, #241, #242

Summary

Create a specialized Elm-to-F# Guru skill that facilitates high-quality migration of Elm code to idiomatic F#, with proactive review capability built-in from day one. This guru combines domain expertise, automation, continuous improvement, and cross-project portability principles from the unified skill framework.

The Elm-to-F# Guru will be the first guru built with review capability from the start, establishing a pattern for future gurus and demonstrating the full power of the guru framework.


1. Proactive Review Capability ⭐ NEW

The Elm-to-F# Guru includes proactive review as a core competency, not an afterthought. This sets it apart from earlier gurus where review capabilities were added later.

What the Guru Reviews

The Elm-to-F# Guru actively monitors migration progress and quality, identifying:

1.1 Anti-Patterns

  • Elm idioms ported literally instead of idiomatically
    • Example: Elm’s Maybe translated directly to Option without considering F#’s ValueOption or nullable reference types where appropriate
    • Example: Elm’s union types with overly verbose F# discriminated unions when simpler patterns exist

1.2 Myriad Plugin Opportunities

  • Patterns appearing 3+ times that should be automated via code generation
    • Example: Repetitive JSON serialization patterns across multiple types
    • Example: Boilerplate for F# record validation that mirrors Elm’s structure
    • Example: Type conversions between Elm and F# representations

1.3 F# Idiom Violations

  • Code using non-idiomatic F# patterns
    • Example: Excessive use of mutable variables when immutable patterns are clearer
    • Example: Missing type annotations in public APIs
    • Example: Not using F# computation expressions where appropriate
    • Example: Ignoring F# pattern matching exhaustiveness

1.4 Migration Anti-Patterns

  • Common mistakes repeated across modules
    • Example: Incorrect type mappings (Elm Int → F# int32 vs int64 vs bigint)
    • Example: Lost type safety during translation
    • Example: Performance issues from naive translations (e.g., list operations)

1.5 Generated Code Safety

  • Verify code generated by Myriad plugins is correct
    • Example: Plugin-generated serializers match hand-written versions
    • Example: Generated validation logic preserves Elm’s semantics
    • Example: Type provider output is AOT-compatible (coordination with AOT Guru)

Review Triggers

The guru performs reviews at multiple cadences:

Session-Based Review (After Each Module Migration)

Trigger: Module migration marked complete
Action: Analyze migration for:
  - Pattern frequency (track repetitions)
  - Idiom compliance (F# best practices)
  - Type safety preservation (Elm → F#)
  - Test coverage (coordinate with QA Tester)
Output: Session summary with patterns discovered

Weekly Pattern Inventory Review

Trigger: Weekly scheduled scan (CI job or manual)
Action: Review all migrations from past week:
  - Aggregate pattern occurrences
  - Identify patterns appearing 3+ times
  - Check for emerging anti-patterns
Output: Weekly pattern report

Quarterly Comprehensive Review

Trigger: End of quarter (Q1, Q2, Q3, Q4)
Action: Deep analysis across all migrations:
  - Pattern frequency trends (increasing/decreasing)
  - Myriad plugin opportunities (automation candidates)
  - Migration quality metrics (idiom compliance, safety)
  - Coordination effectiveness (AOT Guru, QA Tester)
Output: Quarterly review report with improvement recommendations

Review Output Format

Reviews produce structured output for consumption by other gurus and developers:

## Elm-to-F# Migration Review Report
**Date:** 2025-12-19  
**Scope:** Modules migrated since last review  
**Reviewer:** Elm-to-F# Guru

### Pattern Frequency Report
| Pattern | Count | Example Locations | Status |
|---------|-------|-------------------|--------|
| ValueType boxing in pattern matching | 7 | `Module.A:45`, `Module.B:23`, ... | ⚠️ Recommend Myriad plugin |
| Manual JSON serialization | 5 | `Module.C:12`, `Module.D:67`, ... | ⚠️ Consider automation |
| Recursive union type translation | 12 | `Module.E:89`, `Module.F:34`, ... | ✅ Pattern documented |

### Myriad Plugin Recommendations
1. **Auto-Serializer Plugin** (Priority: High)
   - **Pattern:** Manual JSON serialization appears 5+ times
   - **Impact:** Reduce boilerplate, improve consistency
   - **Effort:** ~2-3 days to implement
   - **Token Savings:** ~50 tokens per type × 20 types = ~1000 tokens

2. **ValueType Boxing Detector** (Priority: Medium)
   - **Pattern:** Boxing detected 7 times
   - **Impact:** Performance + AOT compatibility
   - **Effort:** ~1 day to implement detection script
   - **Token Savings:** ~30 tokens per detection × 10/quarter = ~300 tokens

### Automation Script Suggestions
1. **Create `detect-boxing-patterns.fsx`**
   - Scans F# code for ValueType boxing in pattern matches
   - Integrates with AOT Guru's IL analysis
   
2. **Create `validate-type-mappings.fsx`**
   - Verifies Elm → F# type mappings are correct
   - Checks for precision loss (e.g., Elm Int → F# int vs int64)

### Migration Quality Metrics
- **Modules Migrated:** 80
- **Idiom Violations:** 1,200 (decreasing from 1,500 last quarter)
- **Patterns Discovered:** 45 total (12 new this quarter)
- **Test Coverage:** 82% (target: 80%, ✅ on target)
- **AOT Compatibility:** 95% (5% needs Myriad plugins)

### Coordination Status
- **With AOT Guru:** 3 generated code reviews completed, 2 IL warnings resolved
- **With QA Tester:** Test coverage verified, 5 edge cases added
- **With Release Manager:** Migration progress tracked, on schedule for Q1 2026

### Next Quarter Focus
1. Implement auto-serializer Myriad plugin
2. Add boxing detection to quarterly scans
3. Document recursive union type pattern (12 occurrences suggest it's stable)
4. Coordinate with AOT Guru on plugin IL output

2. Automated Feedback & Continuous Improvement

The Elm-to-F# Guru implements a continuous learning loop inspired by the guru framework’s retrospective philosophy.

Session Capture

Every migration session includes a “Patterns Discovered” section:

## Migration Session: Module.BusinessLogic
**Status:** Complete  
**Lines Migrated:** 450  
**F# Output:** 380 lines

### Patterns Discovered
1. **Union Type with Private State**: Elm's opaque types → F# with private constructor pattern
2. **Computation Expression Candidate**: Repeated `Result` chaining → F# `result { }` CE
3. **Myriad Opportunity**: 3rd occurrence of manual JSON serialization for discriminated unions

### Idiom Improvements
- Changed: Mutable loop → `List.fold` (idiomatic F#)
- Fixed: Added explicit type annotations to public API
- Enhanced: Used `ValueOption` instead of `Option` for high-frequency code paths

### Questions for Next Review
- Should we create a Myriad plugin for opaque type translation?
- Is the `Result` computation expression approach consistent with project standards?

Quarterly Reviews

At the end of each quarter, the guru performs a comprehensive pattern review:

Process:

  1. Collect: Gather all “Patterns Discovered” sections from the quarter
  2. Analyze: Identify top 3-5 patterns by frequency
  3. Decide: Determine which patterns warrant automation (Myriad plugin, script, or decision tree update)
  4. Document: Update the guru’s pattern catalog and playbooks
  5. Plan: Set improvement goals for next quarter

Example Quarterly Review Outcomes:

Q1 2025 Review:
- Discovered 15 new patterns (total: 45)
- Top pattern: JSON serialization (appeared 18 times)
- Decision: Create Myriad plugin for auto-serialization
- Playbook updated: Added decision tree for union type translation

Q2 2025 Review:
- Created 2 Myriad plugins (auto-serialization, validation)
- JSON serialization occurrences dropped from 18 → 2 (automation working!)
- New pattern emerged: Recursive tree structures (8 occurrences)
- Decision: Document pattern, not yet frequent enough for plugin

Q3 2025 Review:
- Updated migration decision tree based on Q1-Q2 learnings
- Pattern catalog now has 52 patterns (7 added, no removals)
- Token savings from automation: ~2,500 tokens per quarter
- Coordination with AOT Guru improved (generated code review process)

Playbook Evolution

The guru’s playbooks and decision trees evolve based on learnings:

  • Before: Generic “Translate Elm to F#” steps
  • After Q1: Specific guidance on union types, computation expressions, serialization
  • After Q2: Automation scripts integrated, Myriad plugin usage documented
  • After Q3: Common pitfalls section added, anti-pattern detection automated

Automation Loop

The feedback loop prioritizes automation:

Pattern appears 1-2 times → Document in catalog
Pattern appears 3-5 times → Create detection script + decision tree entry
Pattern appears 6+ times → Strong candidate for Myriad plugin or major automation
Pattern appears 10+ times → Critical to automate (prevent technical debt)

3. Token Efficiency Analysis

The Elm-to-F# Guru includes 3+ F# automation scripts designed to save significant agent tokens by replacing high-cost manual operations.

Script 1: extract-elm-tests.fsx

Purpose: Extract test structure from Elm test files to guide F# test creation

Reusability: ⭐ Highly portable - works for any Elm-to-X migration, not F#-specific

Workflow:

# Input: Elm test file
dotnet fsi .claude/skills/elm-to-fsharp/scripts/extract-elm-tests.fsx tests/Module.elm

# Output: Structured test plan
{
  "module": "Module",
  "testCount": 12,
  "scenarios": [
    {
      "name": "should handle empty list",
      "type": "unit",
      "inputs": ["[]"],
      "expected": "Ok(0)"
    },
    {
      "name": "should sum positive numbers",
      "type": "property",
      "property": "forAll (list Int) (fun xs -> sum xs >= 0)",
      "inputs": ["[1, 2, 3]"],
      "expected": "6"
    }
  ]
}

Token Savings:

  • Manual: Read Elm test file (200 tokens), understand structure (100 tokens), write F# test plan (150 tokens) = 450 tokens
  • Automated: Run script (5 tokens), parse JSON output (25 tokens) = 30 tokens
  • Savings: 420 tokens per module × 80 modules = 33,600 tokens annually

Script 2: analyze-elm-module.fsx

Purpose: Structural analysis of Elm modules to plan F# translation

Reusability: ⭐ Portable - core analysis logic works for any Elm module, F#-specific mappings can be parameterized

Workflow:

# Input: Elm source file
dotnet fsi .claude/skills/elm-to-fsharp/scripts/analyze-elm-module.fsx src/Module.elm

# Output: Translation plan
{
  "module": "Module",
  "types": [
    { "name": "Status", "kind": "union", "variants": ["Loading", "Success Data", "Error String"] },
    { "name": "Config", "kind": "record", "fields": ["timeout: Int", "retries: Int"] }
  ],
  "functions": [
    { "name": "init", "signature": "Config -> Status", "complexity": "low" },
    { "name": "update", "signature": "Msg -> Status -> Status", "complexity": "medium" }
  ],
  "dependencies": ["Http", "Json.Decode"],
  "translationHints": [
    "union type 'Status' → F# discriminated union with private state",
    "record 'Config' → F# record with [<CLIMutable>] if used with JSON",
    "Http dependency → use FsHttp or System.Net.Http.Json"
  ]
}

Token Savings:

  • Manual: Read Elm file (300 tokens), identify types/functions (150 tokens), plan translation (200 tokens) = 650 tokens
  • Automated: Run script (5 tokens), parse output (40 tokens), review hints (50 tokens) = 95 tokens
  • Savings: 555 tokens per module × 80 modules = 44,400 tokens annually

Script 3: verify-migration.fsx

Purpose: Validate F# migration against Elm source for correctness

Reusability: ⚠️ Partially portable - validation logic can be reused, but type mappings are F#-specific

Workflow:

# Inputs: Elm source + F# target
dotnet fsi .claude/skills/elm-to-fsharp/scripts/verify-migration.fsx src/Module.elm src/Module.fs

# Output: Verification report
{
  "module": "Module",
  "status": "warning",
  "typeMappings": [
    { "elm": "Status", "fsharp": "Status", "status": "✅ correct" },
    { "elm": "Config", "fsharp": "Config", "status": "✅ correct" }
  ],
  "functionMappings": [
    { "elm": "init", "fsharp": "init", "status": "✅ correct" },
    { "elm": "update", "fsharp": "update", "status": "⚠️ signature differs" }
  ],
  "issues": [
    {
      "severity": "warning",
      "location": "Module.fs:45",
      "message": "Function 'update' signature differs from Elm: expected 'Msg -> Status -> Status', found 'Msg -> Status -> Status * Cmd'",
      "suggestion": "Verify this is intentional (F# may have richer return type)"
    }
  ],
  "testCoverage": {
    "elm": 12,
    "fsharp": 10,
    "missing": ["should handle empty list", "should reject invalid input"]
  }
}

Token Savings:

  • Manual: Compare Elm + F# (400 tokens), check types (150 tokens), verify functions (200 tokens), test coverage (100 tokens) = 850 tokens
  • Automated: Run script (5 tokens), review report (80 tokens) = 85 tokens
  • Savings: 765 tokens per module × 80 modules = 61,200 tokens annually

Script 4: detect-patterns.fsx (Review Capability)

Purpose: Find anti-patterns and idiom violations in migrated F# code

Reusability: ⚠️ F#-specific - detection rules are F# idiom-specific, but framework is portable

Workflow:

# Input: F# files from migration
dotnet fsi .claude/skills/elm-to-fsharp/scripts/detect-patterns.fsx src/*.fs

# Output: Pattern detection report
{
  "scannedFiles": 25,
  "patterns": [
    {
      "pattern": "ValueType boxing in pattern match",
      "severity": "warning",
      "count": 7,
      "locations": [
        { "file": "Module.fs", "line": 45, "context": "match x with | Some (y: struct ValueType) -> ..." },
        { "file": "Other.fs", "line": 23, "context": "..." }
      ],
      "recommendation": "Use 'ValueOption' instead of 'Option' for value types to avoid boxing"
    },
    {
      "pattern": "Mutable variable in pure function",
      "severity": "info",
      "count": 3,
      "locations": [
        { "file": "Logic.fs", "line": 67, "context": "let mutable acc = 0; for x in xs do acc <- acc + x; acc" }
      ],
      "recommendation": "Consider using 'List.fold' or other functional patterns"
    }
  ],
  "myriadOpportunities": [
    {
      "pattern": "Manual JSON serialization",
      "count": 5,
      "priority": "high",
      "recommendation": "Create Myriad plugin for auto-serialization"
    }
  ]
}

Token Savings:

  • Manual: Read F# files (500 tokens), identify patterns (300 tokens), categorize (150 tokens) = 950 tokens
  • Automated: Run script (5 tokens), review report (100 tokens) = 105 tokens
  • Savings: 845 tokens per review × 4 reviews/quarter = 3,380 tokens per quarter

Total Token Savings (All Scripts)

ScriptPer-Use SavingsFrequencyAnnual Savings
extract-elm-tests.fsx420 tokens80 modules33,600 tokens
analyze-elm-module.fsx555 tokens80 modules44,400 tokens
verify-migration.fsx765 tokens80 modules61,200 tokens
detect-patterns.fsx845 tokens4/quarter × 4 quarters13,520 tokens
Total Annual Savings152,720 tokens

Note: These savings assume 80 modules to migrate over the project lifetime. Actual savings will scale with the number of modules.


4. Cross-Project Portability

The Elm-to-F# Guru is designed with portability in mind, making it easier to adapt to other Elm migration projects or even other functional language migrations.

Portable Components ✅

These components can be reused in other projects with minimal changes:

4.1 Pattern Detection Logic

  • What: Structural analysis of source code (identifying types, functions, dependencies)
  • Portable to: Elm-to-Haskell, Elm-to-OCaml, Elm-to-ReasonML, Elm-to-PureScript
  • Adaptation effort: Low (~1-2 hours to adjust output format)

4.2 Structural Analysis

  • What: Understanding Elm module structure, type definitions, function signatures
  • Portable to: Any Elm-to-X migration
  • Adaptation effort: Very low (~30 minutes, mostly path configuration)

4.3 Idiom Checkers (Framework)

  • What: Framework for detecting anti-patterns and idiom violations
  • Portable to: Any source-to-target language migration
  • Adaptation effort: Medium (~4-8 hours to define target language idioms)

4.4 Review Philosophy and Feedback Loops

  • What: Session capture, quarterly reviews, automation loop, retrospective integration
  • Portable to: Any guru (not specific to Elm-to-F# at all)
  • Adaptation effort: Very low (~1 hour to customize templates)

4.5 Core Review Capability Pattern

  • What: Proactive scanning → issue detection → reporting → improvement cycle
  • Portable to: Any guru (already used by QA Tester, AOT Guru, Release Manager)
  • Adaptation effort: Low (~2-4 hours to define domain-specific scan criteria)

Non-Portable Components ⚠️

These components are specific to F# and morphir-dotnet:

4.6 F#-Specific Idioms

  • What: F# best practices, computation expressions, ValueOption vs Option, etc.
  • Portable to: Other F# projects (yes), other languages (no)
  • Reason: Deeply tied to F# language features

4.7 Myriad Plugin Examples

  • What: F# code generation via Myriad (F#-specific tool)
  • Portable to: Other F# projects (yes), other languages (no, but similar tools exist)
  • Reason: Myriad is F#-specific, though concepts apply to other compile-time code generation tools

4.8 Type System Mappings (Elm → F#)

  • What: Specific rules for translating Elm types to F# types
  • Portable to: Elm-to-Haskell (partial), Elm-to-OCaml (partial), Elm-to-X (needs remapping)
  • Reason: Type systems differ across target languages

Reusable Across Gurus ⭐

Some components are valuable for all gurus, not just migration gurus:

4.9 Automation Script Framework

  • What: F# script structure, argument parsing, JSON output, error handling
  • Reusable by: QA Tester, AOT Guru, Release Manager, future gurus
  • Adaptation effort: Very low (copy template, customize logic)

4.10 Pattern Catalog Structure

  • What: Markdown-based catalog of patterns with examples, pros/cons, recommendations
  • Reusable by: All gurus
  • Adaptation effort: Very low (change domain-specific patterns)

4.11 Quarterly Review Template

  • What: Structured review process with findings, metrics, improvement recommendations
  • Reusable by: All gurus
  • Adaptation effort: Very low (customize review criteria)

Adaptation Guide for Other Projects

If adapting Elm-to-F# Guru to another project:

For Elm-to-Haskell:

  1. ✅ Keep: extract-elm-tests.fsx, analyze-elm-module.fsx (Elm parsing logic)
  2. ✅ Keep: Pattern detection framework, review philosophy
  3. ⚠️ Modify: Type mappings (Elm → Haskell instead of Elm → F#)
  4. ⚠️ Replace: Myriad plugins → Template Haskell or other Haskell code generation
  5. ⚠️ Replace: F# idiom checkers → Haskell idiom checkers (e.g., prefer fmap over liftM)

Estimated adaptation effort: 12-20 hours (2-3 days)

For Elm-to-OCaml:

  1. ✅ Keep: Same as Elm-to-Haskell
  2. ⚠️ Modify: Type mappings (Elm → OCaml)
  3. ⚠️ Replace: Myriad plugins → PPX preprocessors (OCaml code generation)
  4. ⚠️ Replace: F# idiom checkers → OCaml idiom checkers

Estimated adaptation effort: 12-20 hours (2-3 days)

For Python-to-F# (different source language):

  1. ⚠️ Modify: Source language parsing (Python AST instead of Elm)
  2. ✅ Keep: Target language idioms (F#)
  3. ✅ Keep: Review philosophy, pattern detection framework
  4. ✅ Keep: Myriad plugins (F# target)
  5. ⚠️ Replace: Type mappings (Python → F# is more complex due to dynamic typing)

Estimated adaptation effort: 24-40 hours (4-6 days)


5. Guru Coordination

The Elm-to-F# Guru coordinates with other gurus to ensure high-quality, production-ready migrations.

With AOT Guru

Direction: Elm-to-F# → AOT Guru (generated code review)

Coordination Point: After migration, before release

Workflow:

Elm-to-F# Guru: "I've migrated Module.fs and generated serialization code via Myriad plugin"
   ↓
AOT Guru: "Let me review for AOT safety..."
   ↓ [scans IL output]
AOT Guru: "Found IL2026 warning in generated serializer. Recommend using source generator instead."
   ↓
Elm-to-F# Guru: "Updated Myriad plugin to generate AOT-compatible code"
   ↓
AOT Guru: "Verified. No IL warnings. Binary size within target."

Integration Points:

  • IL Warning Analysis: AOT Guru scans generated code for reflection usage (IL2026, IL3050)
  • Binary Size Impact: AOT Guru reports if migration increases binary size beyond targets
  • Plugin Compatibility: AOT Guru verifies Myriad-generated code is AOT-compatible
  • Feedback Loop: AOT findings feed back to Elm-to-F# playbooks (avoid reflection patterns)

Review Coordination:

  • Elm-to-F# quarterly review includes “Generated Code AOT Status” section
  • AOT Guru quarterly review includes “Myriad Plugin Compatibility” section
  • Both gurus participate in joint retrospectives when IL warnings are found

With QA Tester

Direction: Elm-to-F# → QA Tester (test coverage verification)

Coordination Point: After migration, before marking module complete

Workflow:

Elm-to-F# Guru: "I've migrated Module.fs. Original Elm had 12 tests."
   ↓
QA Tester: "Checking F# test coverage..."
   ↓ [runs verify-migration.fsx]
QA Tester: "Found 10/12 tests. Missing: 'should handle empty list', 'should reject invalid input'"
   ↓
Elm-to-F# Guru: "Added missing tests. Coverage now 12/12."
   ↓
QA Tester: "Verified. Coverage 82% (target: 80%). ✅"

Integration Points:

  • Test Extraction: QA Tester validates output of extract-elm-tests.fsx
  • Coverage Verification: QA Tester ensures F# tests match or exceed Elm coverage
  • Edge Case Detection: QA Tester identifies missing edge cases in migrated code
  • Regression Testing: QA Tester runs regression tests after bulk migrations

Review Coordination:

  • Elm-to-F# quarterly review includes “Test Coverage Status” section
  • QA Tester quarterly review includes “Migration Test Quality” section
  • Both gurus collaborate on test plan templates for common migration scenarios

With Release Manager

Direction: Release Manager → Elm-to-F# (version tracking for milestones)

Coordination Point: Release planning and retrospectives

Workflow:

Release Manager: "Planning v1.0.0 release. What's migration status?"
   ↓
Elm-to-F# Guru: "80 modules completed, 20 remaining. On track for Q1 2026."
   ↓
Release Manager: "Noted. Including 'Elm-to-F# migration: 80% complete' in release notes."
   ↓ [release happens]
Release Manager: "v1.0.0 deployed. Any migration-related issues?"
   ↓
Elm-to-F# Guru: "No issues reported. 2 edge cases found in testing, fixed in v1.0.1."

Integration Points:

  • Milestone Tracking: Release Manager tracks migration progress for release notes
  • Version Milestones: Elm-to-F# reports which modules are included in each release
  • Release Notes: Release Manager includes migration status and highlights
  • Retrospectives: Both gurus participate in post-release retrospectives

Review Coordination:

  • Elm-to-F# quarterly review includes “Release Milestones Achieved” section
  • Release Manager quarterly review includes “Feature Parity Progress” section
  • Both gurus align on Q1/Q2/Q3/Q4 migration goals

Common Feedback: Retrospectives

All gurus participate in retrospectives after significant events:

Post-Release Retrospective:

Facilitator: "What went well with the Elm-to-F# migration in v1.0.0?"
Elm-to-F# Guru: "80 modules migrated with no blocking issues. Myriad plugin saved significant time."
QA Tester: "Test coverage stayed above 80%. Found 2 edge cases, both resolved quickly."
AOT Guru: "Generated code is AOT-compatible. Binary size within targets."
Release Manager: "Migration progressed as planned. Communication was clear."

Facilitator: "What could improve?"
Elm-to-F# Guru: "Myriad plugin created IL warnings initially. Need earlier AOT review."
AOT Guru: "Agree. Let's add AOT review to migration checklist before PR approval."
QA Tester: "Some tests were added late. Extract Elm tests earlier in migration workflow."
Release Manager: "Milestone tracking was manual. Automate migration progress reporting."

Facilitator: "Action items?"
ALL: 
  1. Update migration checklist: Add AOT review step
  2. Update migration playbook: Extract Elm tests as step 1 (not step 3)
  3. Create migration-progress.fsx script (automate status reporting)
  4. Next quarter: Monitor if changes reduce issues

Retrospective Outputs Feed Back to Gurus:

  • Elm-to-F# Guru: Updates playbooks, adds AOT review step, documents new patterns
  • AOT Guru: Updates review criteria to include Myriad plugin output
  • QA Tester: Updates test plan template to prioritize early test extraction
  • Release Manager: Automates migration progress tracking for release notes

6. Review Integration with Retrospectives

The Elm-to-F# Guru combines proactive review (finding issues before they cause problems) with reactive retrospectives (learning from problems that occurred).

How They Work Together

┌─────────────────────────────────────────────────────────────┐
│                    CONTINUOUS IMPROVEMENT CYCLE              │
└─────────────────────────────────────────────────────────────┘

Q1 REVIEWS (Proactive):
  Findings:
    - "ValueType boxing pattern found in 7 places"
    - "Elm pattern 'Result.andThen chains' not idiomatic in F#"
    - "3 modules using old F# style (mutable loops)"
    - "Myriad plugin opportunity: JSON serialization (5 occurrences)"

         ↓ Feed into retrospectives

Q1 RETROSPECTIVES (Reactive):
  Questions:
    - "Why does ValueType boxing happen?"
      → Root cause: Developers unaware of ValueOption vs Option
    - "Are we teaching F# idioms correctly?"
      → Root cause: Migration playbook lacks idiom guidance
    - "Should we automate this pattern?"
      → Root cause: Repetitive manual work → errors

         ↓ Decisions & Actions

Q1 OUTCOMES:
  Actions:
    1. Create Myriad plugin for auto-serialization (eliminates repetitive manual work)
    2. Update migration decision tree:
       - Add "ValueOption vs Option" decision point
       - Document Elm Result → F# Result CE pattern
    3. Add pattern detection to verify.fsx (prevent old F# style from recurring)
    4. Update playbooks:
       - Add "F# Idioms" section with examples
       - Include checklist: "Did you consider computation expressions?"

         ↓ Improvements deployed

Q2 REVIEWS (Next Cycle):
  Findings:
    - "ValueType boxing reduced from 7 → 2 occurrences" ✅ Improvement!
    - "0 old F# style issues (automated detection working)" ✅ Improvement!
    - "JSON serialization: 5 → 0 occurrences (Myriad plugin working)" ✅ Improvement!
    - "New pattern discovered: Recursive tree structures (4 modules)"

         ↓ New questions, new cycle

Example Integration: ValueType Boxing Pattern

Quarter 1: Discovery

Review Findings (Proactive):

Pattern: ValueType boxing in pattern matching
Occurrences: 7
Locations: Module.A:45, Module.B:23, Module.C:67, ...
Severity: Warning
Impact: Performance degradation + AOT compatibility concerns
Recommendation: Use ValueOption instead of Option for value types

Retrospective Analysis (Reactive):

Question: "Why does ValueType boxing happen so frequently?"

Investigation:
- Reviewed 7 occurrences
- Pattern: All in code migrated from Elm's Maybe type
- Root cause: Migration playbook says "Elm Maybe → F# Option" (generic)
- Developers followed playbook literally without considering performance

Conclusion: Playbook lacks guidance on ValueOption vs Option choice

Q1 Outcomes:

Action 1: Update migration playbook
  Before: "Elm Maybe → F# Option"
  After: "Elm Maybe → F# Option (reference types) or ValueOption (value types)"
  Added: Decision tree with examples

Action 2: Create detection script
  Script: detect-boxing-patterns.fsx
  Integration: Run as part of verify-migration.fsx
  Output: Warning if Option used with value types

Action 3: Document pattern
  Added to pattern catalog: "Pattern #23: ValueOption for Value Types"
  Examples: 7 real cases from Q1 migrations
  Guideline: "Use ValueOption<int>, ValueOption<DateTime> to avoid boxing"

Quarter 2: Validation

Review Findings (Proactive):

Pattern: ValueType boxing in pattern matching
Occurrences: 2 (down from 7) ✅
Locations: Module.X:89, Module.Y:12
Severity: Warning
Status: IMPROVING (71% reduction)
Note: 2 occurrences are in legacy code, not new migrations

Retrospective Analysis (Reactive):

Question: "Why do 2 occurrences still exist?"

Investigation:
- Both in legacy code (pre-Q1 improvements)
- Not flagged because verify-migration.fsx only runs on new migrations
- Opportunity: Run detection script on entire codebase, not just new code

Conclusion: Expand automated detection to full codebase

Q2 Outcomes:

Action 1: Expand detection scope
  Before: verify-migration.fsx runs only on new migrations
  After: detect-patterns.fsx runs on entire codebase weekly

Action 2: Fix legacy code
  Created PRs to fix 2 legacy occurrences
  Added to backlog: "Modernize legacy code patterns"

Action 3: Celebrate improvement
  Shared success with team: "ValueType boxing reduced 71% via playbook updates"

Quarter 3: Stability

Review Findings (Proactive):

Pattern: ValueType boxing in pattern matching
Occurrences: 0 ✅
Severity: N/A (no longer occurring)
Status: RESOLVED
Note: Pattern detection active, no new occurrences in Q3

Retrospective Analysis (Reactive):

Question: "What made this improvement successful?"

Reflection:
- Proactive review discovered the pattern early (Q1)
- Retrospective identified root cause (playbook gap)
- Combined action: Updated playbook + automated detection
- Validation: Q2 review confirmed improvement, Q3 confirmed resolution

Conclusion: Review + Retrospective cycle works! Apply to other patterns.

Q3 Outcomes:

Action 1: Document success
  Added case study to guru-creation-guide.md: "ValueType Boxing Pattern Resolution"
  Template for future pattern improvements

Action 2: Apply learnings to new pattern
  Q3 discovered: "Recursive tree structure pattern (8 occurrences)"
  Following same process: Review → Retrospective → Action → Validate

Review vs Retrospective: Key Differences

AspectProactive ReviewReactive Retrospective
TimingScheduled (weekly, quarterly) or continuousAfter events (failures, releases)
FocusFind issues before they cause problemsUnderstand why problems occurred
InputScans, metrics, automated analysisIncidents, failures, team feedback
OutputFindings, recommendations, metricsRoot causes, lessons learned
ActionPreventive measures (detection scripts)Corrective measures (process changes)
Example“Found 7 boxing patterns”“Why did boxing happen? Playbook gap.”

Mutual Benefits

Reviews inform retrospectives:

  • Review findings become retrospective discussion topics
  • Pattern frequency data helps prioritize retrospective focus
  • Metrics show whether improvements are working

Retrospectives improve reviews:

  • Root cause analysis refines what reviews should look for
  • Process insights suggest new review criteria
  • Team feedback identifies blind spots in automated reviews

Together:

  • Reviews catch issues early (prevent problems)
  • Retrospectives understand why issues occur (prevent recurrence)
  • Continuous cycle drives improvement quarter-over-quarter

7. Enhanced Success Criteria

The Elm-to-F# Guru defines success across functional, learning, automation, and maturity dimensions, with clear metrics for each phase.

Functional Criteria ✅

Core functionality that must work:

  • Elm modules successfully migrated to F#

    • All Elm syntax translated to valid F# code
    • Code compiles without errors
    • Type safety preserved (no unsafe casts or failwith unless in Elm source)
  • Generated code compiles and tests pass

    • F# output passes dotnet build
    • All tests from Elm are represented in F# (via extract-elm-tests.fsx)
    • Tests pass with same semantics as Elm tests
  • Myriad plugins operational

    • Plugins generate valid F# code
    • Generated code is AOT-compatible (verified by AOT Guru)
    • Plugins integrated into build process

Learning Criteria 📚

Evidence of continuous improvement:

  • 20+ patterns documented in pattern catalog

    • Each pattern includes: name, description, examples, pros/cons, recommendations
    • Patterns categorized: type translation, function translation, idioms, anti-patterns
    • Catalog updated quarterly
  • 5+ Myriad plugins implemented (from patterns)

    • Plugins created for patterns appearing 6+ times
    • Examples: auto-serialization, validation, union type helpers
    • Each plugin documented with usage guide
  • Quarterly reviews showing pattern frequency trends

    • Q1 baseline established
    • Q2/Q3/Q4 show trends (increasing/decreasing)
    • Improvements correlated with actions taken (e.g., “boxing reduced 71% after playbook update”)
  • Migration decision tree improved 3+ times based on learnings

    • Q1: Initial decision tree (basic Elm → F# mappings)
    • Q2: Updated with ValueOption vs Option guidance
    • Q3: Added computation expression decision points
    • Q4: Expanded with recursive type handling
    • Evidence: Git history shows decision tree evolution

Automation Criteria 🤖

Scripts and automation in place:

  • 3 core F# scripts (extract, analyze, verify)

    • extract-elm-tests.fsx: Working and tested
    • analyze-elm-module.fsx: Working and tested
    • verify-migration.fsx: Working and tested
    • All scripts have JSON output option
    • All scripts have error handling and help text
  • 1+ review/detection scripts live

    • detect-patterns.fsx: Working and integrated
    • Runs weekly or on-demand
    • Produces actionable reports
    • False positive rate < 10%
  • Token savings measured and documented

    • Baseline manual costs documented
    • Automated costs measured
    • Savings calculated per script
    • Annual savings: 150,000+ tokens (see Section 3)

Maturity Phases 🎯

The guru progresses through phases over time:

Phase 1: Alpha (Manual Migration, Pattern Capturing)

Duration: Q1 (first 3 months of use)

Criteria:

  • Directory structure created (.claude/skills/elm-to-fsharp/)
  • skill.md complete (1000+ lines)
  • README.md and MAINTENANCE.md created
  • 3 core automation scripts working
  • 10+ seed patterns documented
  • Manual migration workflow established
  • First module migrated successfully
  • Pattern capture template in use

Characteristics:

  • High manual effort (guru guides, but humans do most work)
  • Pattern discovery is primary focus
  • Scripts are helpers, not fully automated
  • Quarterly review captures learnings

Success Metric: 10+ modules migrated, 15+ patterns discovered


Phase 2: Beta (Review Capability Working, Myriad Plugins Created)

Duration: Q2-Q3 (months 4-9)

Criteria:

  • Review capability implemented (detect-patterns.fsx)
  • Review scripts tested on real data (10+ modules)
  • Feedback mechanism working (session capture + quarterly reviews)
  • First quarterly review completed with actionable findings
  • 20+ patterns in catalog (10 baseline + 10 new)
  • 2-3 Myriad plugins implemented (from frequent patterns)
  • Decision tree improved based on Q1 learnings
  • Coordination with AOT Guru and QA Tester tested

Characteristics:

  • Automation emerging (Myriad plugins reduce manual work)
  • Proactive review finds issues before they accumulate
  • Patterns guide most decisions (less ad-hoc translation)
  • Integration with other gurus proven

Success Metric: 40+ modules migrated, 3 Myriad plugins live, pattern frequency trending down (automation working)


Phase 3: Stable (Automated Patterns, Predictable Quarterly Improvement)

Duration: Q4+ (month 10 onwards)

Criteria:

  • 25+ patterns in catalog
  • 5+ Myriad plugins operational
  • Review capability proven reliable (4+ reviews completed)
  • Automated feedback generating insights quarterly
  • 3+ quarters of successful evolution (Q1 → Q4)
  • Token efficiency documented (150,000+ tokens saved)
  • Cross-project reuse strategy documented
  • Continuous improvement cycle established (review → retrospective → action → validate)
  • Integration with other gurus seamless

Characteristics:

  • High automation (many patterns handled by Myriad plugins)
  • Predictable quarterly improvements (process refined)
  • Review findings feed back smoothly (no manual intervention)
  • New patterns emerge slowly (most common cases automated)

Success Metric: 80+ modules migrated, pattern discovery rate stabilizes, quarterly improvements sustain


Phase Transition Criteria

Alpha → Beta:

  • Triggered by: First quarterly review complete
  • Required: 10+ modules migrated, 15+ patterns documented, 3 scripts operational
  • Validation: Team feedback confirms guru is useful (not just experimental)

Beta → Stable:

  • Triggered by: Third quarterly review complete (end of Q3)
  • Required: 40+ modules migrated, 20+ patterns, 3+ Myriad plugins, review capability working
  • Validation: Pattern frequency shows automation is working (e.g., JSON serialization 18 → 2 occurrences)

Stable → Excellence (future):

  • Triggered by: After 2+ years of use
  • Required: 100+ modules, 30+ patterns, 10+ Myriad plugins, cross-project reuse proven
  • Validation: Guru is used by other projects (Elm-to-Haskell, Elm-to-OCaml, etc.)

Success Metrics Summary

MetricAlpha (Q1)Beta (Q2-Q3)Stable (Q4+)
Modules Migrated10+40+80+
Patterns in Catalog15+20+25+
Myriad Plugins02-35+
Automation Scripts345+
Token Savings (Annual)N/A (baseline)~75K~150K
Review FrequencyManual (Q1 only)Weekly + QuarterlyWeekly + Quarterly
Quarterly ImprovementsPattern discoveryAutomation + playbook updatesSustained refinement
CoordinationAd-hocTestedSeamless

Acceptance Criteria

This issue enhancement is complete when:

  • Issue #240 body updated with all 7 sections

    • Section 1: Proactive Review Capability ⭐
    • Section 2: Automated Feedback & Continuous Improvement
    • Section 3: Token Efficiency Analysis (4 F# scripts)
    • Section 4: Cross-Project Portability
    • Section 5: Guru Coordination (AOT, QA, Release Manager)
    • Section 6: Review Integration with Retrospectives
    • Section 7: Enhanced Success Criteria (maturity phases)
  • Review capability prominently featured

    • Review is Section 1 (not hidden or afterthought)
    • Review triggers documented (session, weekly, quarterly)
    • Review output format includes examples
    • Review integration with retrospectives illustrated
  • Links to related issues established

  • F# script list specified with token savings estimates

    • 4 scripts: extract-elm-tests.fsx, analyze-elm-module.fsx, verify-migration.fsx, detect-patterns.fsx
    • Token savings per script documented
    • Annual savings calculated: 152,720 tokens
  • Review triggers and output format documented

    • Triggers: Session-based, weekly, quarterly
    • Output format: Markdown report with structured findings
    • Example output provided in Section 1
  • Guru coordination matrix with review integration points

    • AOT Guru: Generated code IL review
    • QA Tester: Test coverage verification
    • Release Manager: Milestone tracking
    • Retrospectives: Common feedback hub
  • Success criteria measurable, time-bound, and includes review metrics

    • Functional, Learning, Automation, Maturity criteria defined
    • Phases: Alpha (Q1), Beta (Q2-Q3), Stable (Q4+)
    • Metrics: modules migrated, patterns documented, plugins created, token savings
  • Retrospective + Review integration illustrated with example

    • Section 6 provides Q1-Q3 cycle example
    • ValueType boxing pattern case study
    • Shows how review findings → retrospective analysis → improvements → validation

Implementation Checklist

When implementing the Elm-to-F# Guru based on this enhanced specification:

Planning Phase (Before Code)

  • Review this document with maintainers
  • Confirm Myriad plugins are acceptable approach
  • Identify 80 Elm modules for migration (prioritize by complexity)
  • Set up tracking issue for quarterly reviews
  • Create .claude/skills/elm-to-fsharp/ directory

Alpha Phase Implementation (Q1)

  • Create skill.md (1000+ lines) following guru template
  • Create README.md and MAINTENANCE.md
  • Implement 3 core scripts: extract-elm-tests.fsx, analyze-elm-module.fsx, verify-migration.fsx
  • Document 10 seed patterns from existing Elm code
  • Migrate first 10 modules manually (capture patterns in session notes)
  • Complete Q1 review: Identify top 3 patterns for automation

Beta Phase Implementation (Q2-Q3)

  • Implement detect-patterns.fsx (review capability)
  • Create 2-3 Myriad plugins (based on Q1 pattern frequency)
  • Update decision tree with Q1 learnings
  • Coordinate with AOT Guru on generated code review
  • Coordinate with QA Tester on test coverage verification
  • Migrate 30 more modules (total: 40)
  • Complete Q2 and Q3 reviews: Track pattern frequency trends

Stable Phase Implementation (Q4+)

  • Implement 2-3 more Myriad plugins (total: 5+)
  • Expand pattern catalog to 25+ patterns
  • Document token savings (validate 150K+ tokens annually)
  • Create cross-project portability guide (for Elm-to-Haskell, etc.)
  • Migrate remaining 40 modules (total: 80)
  • Complete Q4 review: Demonstrate sustained improvement

Documentation & Integration

  • Add Elm-to-F# Guru to .agents/skills-reference.md
  • Update .agents/skill-matrix.md with maturity tracking
  • Add to .agents/capabilities-matrix.md for cross-agent compatibility
  • Reference in AGENTS.md for discoverability
  • Create release notes summarizing guru capabilities

References


Last Updated: 2025-12-19
Status: Enhanced Issue Specification Ready for Implementation
Next Steps: Update GitHub Issue #240 with this content

16.1.8 -

Issue #240 Enhancement - Navigation Guide

This directory contains the enhanced specification for Issue #240: Create Elm to F# Guru Skill, incorporating guru framework principles from Issue #253.

Quick Start

New to Issue #240 enhancement? Start here:

  1. Quick Summary - 10-minute read

    • Overview of all 7 enhancement sections
    • Key features and benefits
    • Before vs After comparison
  2. Full Specification - 30-minute read

    • Complete detailed specification
    • All 7 sections with examples and workflows
    • Implementation checklists

Document Structure

issue-240-summary.md

Purpose: Quick reference and overview
Audience: Maintainers, reviewers, developers
Length: 312 lines (~10 pages)

Contains:

  • Summary of all 7 enhancements
  • Key metrics and benefits
  • Before vs After comparison table
  • Implementation checklist
  • How to use the enhancement

Use this if:

  • You need a quick overview
  • You’re reviewing the enhancement
  • You want to understand what changed

issue-240-enhanced.md

Purpose: Complete specification for implementation
Audience: Developers implementing the guru
Length: 1,167 lines (~45 pages)

Contains:

  • Section 1: Proactive Review Capability ⭐

    • What the guru reviews (anti-patterns, Myriad opportunities, idiom violations)
    • Review triggers (session, weekly, quarterly)
    • Review output format with examples
  • Section 2: Automated Feedback & Continuous Improvement

    • Session capture with “Patterns Discovered” section
    • Quarterly reviews and playbook evolution
    • Automation loop (patterns → scripts → prevention)
  • Section 3: Token Efficiency Analysis

    • 4 F# scripts with detailed workflows
    • Token savings per script and annually (152,720 tokens)
    • JSON output examples
  • Section 4: Cross-Project Portability

    • Portable components (pattern detection, analysis, review philosophy)
    • Non-portable components (F# idioms, Myriad plugins)
    • Adaptation guides (Elm-to-Haskell, Elm-to-OCaml, Python-to-F#)
  • Section 5: Guru Coordination

    • With AOT Guru (generated code review)
    • With QA Tester (test coverage verification)
    • With Release Manager (milestone tracking)
    • Common retrospectives
  • Section 6: Review Integration with Retrospectives

    • How proactive reviews and reactive retrospectives work together
    • Q1-Q3 improvement cycle example
    • ValueType boxing pattern case study
  • Section 7: Enhanced Success Criteria

    • Functional, Learning, Automation, Maturity criteria
    • 3 maturity phases: Alpha (Q1), Beta (Q2-Q3), Stable (Q4+)
    • Measurable metrics and timelines

Use this if:

  • You’re implementing the Elm-to-F# Guru
  • You need detailed workflows and examples
  • You want to understand the full design

How to Use These Documents

For Maintainers

  1. Review issue-240-summary.md for overview
  2. Read issue-240-enhanced.md for details
  3. Use content to update GitHub Issue #240
  4. Assign to developer for implementation

For Developers

  1. Start with issue-240-summary.md to understand scope
  2. Use issue-240-enhanced.md as implementation spec
  3. Follow Implementation Checklist in Section 7
  4. Reference Guru Creation Guide
  5. Use Skill Template

For Reviewers

  1. Check issue-240-summary.md for acceptance criteria
  2. Verify all 7 sections are implemented
  3. Validate automation scripts exist and work
  4. Confirm review capability is functional
  5. Ensure maturity phase criteria are met

Guru Framework Documentation

Code Generation Issues

  • Issue #241 - Create CodeGeneration Project
  • Issue #242 - Integrate Fabulous.AST for F# Code Generation

Implementation Resources

Key Innovations

This enhancement is notable for several innovations:

  1. First Guru with Review Built-In from Day One

    • Earlier gurus (QA Tester, AOT Guru, Release Manager) added review later
    • Elm-to-F# Guru has review as core competency from the start
    • Establishes pattern for all future gurus
  2. Comprehensive Token Efficiency Analysis

    • 4 automation scripts with detailed token savings
    • Per-script and annual projections (152,720 tokens)
    • Reusability across projects documented
  3. Cross-Project Portability Analysis

    • Clear separation: portable vs non-portable components
    • Adaptation guides for Elm-to-Haskell, Elm-to-OCaml, Python-to-F#
    • Effort estimates for adaptation (12-40 hours)
  4. Review + Retrospective Integration

    • Detailed Q1-Q3 improvement cycle example
    • ValueType boxing pattern case study
    • Shows how proactive + reactive approaches work together
  5. Maturity Model with Clear Metrics

    • 3 phases: Alpha (Q1), Beta (Q2-Q3), Stable (Q4+)
    • Measurable success criteria per phase
    • Transition criteria between phases

Questions?


Last Updated: 2025-12-19
Status: ✅ Complete and ready for use
Next Steps: Use content to update GitHub Issue #240

16.1.9 -

Issue #240 Enhancement Summary

Quick Reference: This document summarizes the enhancements to Issue #240 based on guru framework principles from Issue #253.

Document Location

Full Enhancement Document: issue-240-enhanced.md

What Changed

Issue #240 was enhanced to transform the Elm-to-F# Guru from a basic migration tool into a comprehensive, learning-enabled guru with proactive review capability built-in from day one.

Key Enhancements

1. Proactive Review Capability ⭐ (NEW)

What it does:

  • Actively scans migrated code for anti-patterns, idiom violations, and automation opportunities
  • Runs after each module migration (session-based), weekly, and quarterly
  • Identifies patterns appearing 3+ times as Myriad plugin candidates

Why it matters:

  • First guru built with review capability from the start
  • Prevents technical debt before it accumulates
  • Drives automation decisions (patterns → plugins)

Example output:

Pattern Frequency Report:
- ValueType boxing: 7 occurrences → Recommend Myriad plugin
- Manual JSON serialization: 5 occurrences → Consider automation
- Migration quality: 82% idiom compliance (target: 80%) ✅

2. Automated Feedback & Continuous Improvement

What it does:

  • Captures patterns discovered in every migration session
  • Performs quarterly reviews to identify top improvements
  • Updates playbooks and decision trees based on learnings

Why it matters:

  • Ensures the guru gets smarter over time
  • Prevents repeated mistakes across modules
  • Creates a feedback loop: patterns → automation → fewer patterns

Example:

Q1: Discovered 15 patterns, JSON serialization appeared 18 times
Q2: Created Myriad plugin for JSON serialization
Q3: JSON serialization occurrences dropped to 2 (89% reduction)

3. Token Efficiency Analysis

What it does:

  • Provides 4 F# automation scripts targeting high-token-cost tasks
  • Documents token savings per script with annual projections

Scripts:

  1. extract-elm-tests.fsx - Extract test structure from Elm (saves ~420 tokens/module)
  2. analyze-elm-module.fsx - Structural analysis for translation planning (saves ~555 tokens/module)
  3. verify-migration.fsx - Validate F# against Elm source (saves ~765 tokens/module)
  4. detect-patterns.fsx - Find anti-patterns and idiom violations (saves ~845 tokens/review)

Total savings: ~152,720 tokens annually (80 modules)

Why it matters:

  • Automation scripts are reusable across projects (not just morphir-dotnet)
  • Significant efficiency gains for AI agents
  • Clear ROI for guru creation effort

4. Cross-Project Portability

What it does:

  • Documents which components are portable to other Elm-to-X migrations
  • Provides adaptation guides for Elm-to-Haskell, Elm-to-OCaml, etc.

Portable components:

  • ✅ Pattern detection logic (works for any Elm source)
  • ✅ Structural analysis (Elm module parsing)
  • ✅ Review philosophy (applies to all gurus)
  • ✅ Automation script framework (F# script structure)

Non-portable components:

  • ⚠️ F#-specific idioms
  • ⚠️ Myriad plugins (F#-specific tool)
  • ⚠️ Type mappings (Elm → F# specific)

Adaptation effort: 12-20 hours for Elm-to-Haskell, 12-20 hours for Elm-to-OCaml

Why it matters:

  • Reduces cost of creating similar gurus for other languages
  • Establishes patterns that other migration projects can follow
  • Increases ROI of guru framework investment

5. Guru Coordination

What it does:

  • Defines how Elm-to-F# Guru coordinates with AOT Guru, QA Tester, and Release Manager
  • Establishes clear integration points and workflows

Coordination examples:

With AOT Guru:

Elm-to-F# generates code → AOT Guru reviews for IL warnings → 
Feedback: "Found IL2026, use source generator" → 
Elm-to-F# updates plugin → AOT Guru verifies: "✅ No warnings"

With QA Tester:

Elm-to-F# migrates module → QA Tester checks coverage →
Feedback: "10/12 tests, missing 2 edge cases" →
Elm-to-F# adds tests → QA Tester: "✅ 12/12 coverage"

With Release Manager:

Release Manager: "What's migration status for v1.0.0?"
Elm-to-F# Guru: "80/100 modules complete, on track for Q1 2026"
Release Manager: "Noted, including in release notes"

Why it matters:

  • No guru works in isolation
  • Cross-guru coordination ensures quality
  • Shared retrospectives drive project-wide improvements

6. Review Integration with Retrospectives

What it does:

  • Shows how proactive reviews and reactive retrospectives work together
  • Provides Q1-Q3 example of the improvement cycle

Cycle:

Q1 Reviews (Proactive): "Found 7 ValueType boxing patterns"
   ↓
Q1 Retrospectives (Reactive): "Why? Playbook lacks ValueOption guidance"
   ↓
Q1 Outcomes: Update playbook, create detection script
   ↓
Q2 Reviews: "Boxing reduced from 7 → 2 (71% improvement)"
   ↓
Q3 Reviews: "Boxing at 0, pattern resolved"

Why it matters:

  • Reviews find issues early (prevent problems)
  • Retrospectives find root causes (prevent recurrence)
  • Together they create a continuous improvement cycle

7. Enhanced Success Criteria

What it does:

  • Defines success across 4 dimensions: Functional, Learning, Automation, Maturity
  • Establishes 3 maturity phases: Alpha, Beta, Stable

Maturity phases:

PhaseTimelineKey Criteria
AlphaQ1 (months 1-3)10+ modules migrated, 15+ patterns, 3 scripts
BetaQ2-Q3 (months 4-9)40+ modules, 20+ patterns, 2-3 Myriad plugins, review working
StableQ4+ (month 10+)80+ modules, 25+ patterns, 5+ plugins, sustained improvement

Success metrics:

  • Modules migrated: 10 → 40 → 80
  • Patterns documented: 15 → 20 → 25+
  • Myriad plugins: 0 → 2-3 → 5+
  • Token savings: Baseline → ~75K → ~150K annually

Why it matters:

  • Clear roadmap for guru evolution
  • Measurable progress indicators
  • Time-bound expectations (quarterly milestones)

Comparison: Before vs After Enhancement

AspectBefore (Original #240)After (Enhanced #240)
Review CapabilityNot mentioned⭐ Built-in from day one (Section 1)
Learning & FeedbackImplicitExplicit quarterly review process (Section 2)
Automation ScriptsGeneric mention4 specific scripts with token savings (Section 3)
PortabilityNot addressedDetailed reusability analysis (Section 4)
Guru CoordinationNot definedClear workflows with 3 gurus (Section 5)
RetrospectivesNot integratedFull integration with review cycle (Section 6)
Success CriteriaBasic (migrate code)4 dimensions, 3 phases, measurable metrics (Section 7)
Maturity ModelNot presentAlpha → Beta → Stable progression
Token EfficiencyNot quantified152,720 tokens saved annually

Implementation Checklist

When using this enhancement to implement Issue #240:

Phase 0: Planning

  • Read full enhancement document (issue-240-enhanced.md)
  • Review with maintainers
  • Set up tracking issue for quarterly reviews

Phase 1: Alpha (Q1)

  • Create guru directory structure (.claude/skills/elm-to-fsharp/)
  • Implement 3 core scripts (extract, analyze, verify)
  • Migrate 10 modules manually
  • Document 15+ patterns
  • Complete Q1 review

Phase 2: Beta (Q2-Q3)

  • Implement review capability (detect-patterns.fsx)
  • Create 2-3 Myriad plugins
  • Migrate 30 more modules (total: 40)
  • Update decision tree
  • Complete Q2 and Q3 reviews

Phase 3: Stable (Q4+)

  • Create 2-3 more Myriad plugins (total: 5+)
  • Migrate remaining 40 modules (total: 80)
  • Document token savings (validate 150K+ target)
  • Complete Q4 review
  • Document cross-project portability

  • Issue #253 - Unified Cross-Agent AI Skill Framework Architecture (source of guru principles)
  • Issue #254 - Cross-Agent Skill Accessibility & Consolidation
  • Issue #255 - Guru Creation Guide & Skill Template
  • Issue #241 - Create CodeGeneration Project
  • Issue #242 - Integrate Fabulous.AST for F# Code Generation

How to Use This Enhancement

For Maintainers

  1. Review the full enhancement document: issue-240-enhanced.md
  2. Update GitHub Issue #240 with content from the enhanced document
  3. Link related issues (#253, #254, #255, #241, #242)
  4. Assign to developer for implementation

For Developers

  1. Read this summary for quick overview
  2. Read full enhancement for detailed specifications
  3. Follow implementation checklist
  4. Use guru creation guide: .agents/guru-creation-guide.md
  5. Reference skill template: .claude/skills/template/

For Reviewers

  1. Check that all 7 enhancement sections are addressed
  2. Verify automation scripts are implemented
  3. Confirm review capability is working
  4. Validate maturity phase criteria are met
  5. Ensure coordination with other gurus is tested

Benefits of This Enhancement

For the Elm-to-F# Guru

  • Clear roadmap from Alpha → Beta → Stable
  • Built-in learning and improvement mechanisms
  • Coordination with other gurus from day one
  • Quantified success metrics

For the Project

  • First guru with proactive review capability from start
  • Establishes pattern for future gurus
  • Token efficiency gains: 152,720+ annually
  • Reduces technical debt through early detection

For Other Projects

  • Highly portable pattern detection and analysis scripts
  • Reusable review philosophy and feedback loops
  • Adaptation guides for Elm-to-Haskell, Elm-to-OCaml, etc.
  • Demonstrates ROI of guru framework

Next Steps

  1. Update GitHub Issue #240 with content from issue-240-enhanced.md
  2. Link related issues (#253, #254, #255, #241, #242)
  3. Begin Alpha implementation (Q1 phase)
  4. Track progress via quarterly reviews
  5. Share learnings with team and community

Document Status: ✅ Complete
Full Enhancement: issue-240-enhanced.md
Last Updated: 2025-12-19
Created By: GitHub Copilot
Reviewed By: Pending maintainer review

16.2 - QA & Testing

Test plans, quality assurance practices, and testing documentation

This section contains quality assurance documentation, test plans, and testing practices for Morphir .NET.

Test Plans

DocumentDescription
Phase 1 Test PlanInitial test plan for Phase 1 features
Copilot Skill Emulation Test PlanBDD scenarios for GitHub Copilot skill emulation

Test Reports

DocumentDescription
Copilot Skill Emulation Execution ReportResults from skill emulation testing
Copilot Scenarios RunnerAutomated scenario execution documentation

Internal tools

DocumentDescription
WASM host qualificationThe internal, experimental mck wasm-host commands

Testing Practices

Test-Driven Development (TDD)

All development in morphir-dotnet follows TDD:

  1. RED: Write a failing test first
  2. GREEN: Write minimal code to pass the test
  3. REFACTOR: Improve code while keeping tests green

Test Types

TypeFrameworkPurpose
Unit TestsTUnitIndividual component testing
BDD TestsReqnrollBehavior specification and acceptance
Property TestsFsCheckProperty-based verification
Integration TestsTUnitCross-component integration

Test Isolation

Tests run in parallel and must not touch the developer’s machine. Run child processes through TestSandbox and ChildProcess, clean up with TolerantDirectory, and add the machine tripwire to process-heavy test projects. See Test Isolation.

Coverage Requirements

  • Minimum Coverage: 80% for all new code
  • Critical Paths: 100% coverage for IR handling, validation, and CLI commands
  • Regression Prevention: All bug fixes require accompanying tests

Running Tests

# Run all tests
dotnet test --nologo

# Run with coverage
dotnet test --collect:"XPlat Code Coverage"

# Run specific test project
dotnet test tests/Morphir.Core.Tests

16.2.1 - Test Isolation

Keep tests hermetic: sandboxes, child processes, cleanup and the machine tripwire

Test runners run tests in parallel by default. A test that reads your home directory, leaves a child process running, or shares temp paths passes on one machine and fails on another. Use the shared helpers in tests/Morphir.TestSupport to keep each test inside its own sandbox.

Run Morphir in a sandbox

TestSandbox creates a temporary tree with its own home folder, machine configuration folders and work area. Apply confines a child process to that tree:

  • It points HOME, USERPROFILE, APPDATA, LOCALAPPDATA, PROGRAMDATA, XDG_CONFIG_HOME and XDG_CACHE_HOME into the sandbox.
  • It sets MORPHIR_HOME, MORPHIR_DISCOVERY_BOUNDARY (the sandbox root) and MORPHIR_ISOLATED=1, and removes every other inherited MORPHIR_* variable.
  • It keeps NUGET_PACKAGES pointing at your real package cache, so dotnet children don’t download packages again.
  • It disables MSBuild node reuse, so no build node outlives the test and keeps its directory locked.
await using var sandbox = TestSandbox.Create();
var project = Directory.CreateDirectory(sandbox.PathOf("work", "app")).FullName;

var info = ChildProcess.StartInfo("dotnet", "--version");
info.WorkingDirectory = project;
sandbox.Apply(info);

var result = await ChildProcess.RunAsync(info, TimeSpan.FromSeconds(60));
result.ExitCode.Should().Be(0, result.CombinedOutput);

To run the Morphir CLI the same way, start dotnet with the path to morphir.dll and the command’s arguments.

SandboxOptions changes the defaults:

OptionDefaultUse
IsolatedtrueSet to false for a test that exercises machine or personal configuration. Write those files inside the sandbox, for example under sandbox.MorphirHome.
RedirectHometrueSet to false only for a child that needs the real SDK state. Morphir stays contained either way.
Boundarysandbox rootBound discovery more tightly, for example to one project.

sandbox.MorphirHome is not created for you; each test decides whether a global Morphir home exists. Code that takes an explicit environment instead of reading the process uses sandbox.EnvironmentSnapshot(...).

Never call Environment.SetEnvironmentVariable or Directory.SetCurrentDirectory in a test. Both change the whole test process, and other tests are running at the same time.

Start child processes with ChildProcess

ChildProcess.RunAsync reads stdout and stderr concurrently, so a chatty child can’t deadlock the test. When a child times out or the test is cancelled, it kills the child’s whole process tree and waits for it to exit before returning. A process that is still dying would otherwise keep its working directory locked on Windows.

Every call goes through a shared gate that limits how many children run at once. The timeout starts only once the call holds a slot, so time spent waiting doesn’t eat into a test’s budget. The default limit is half the processor count, and never fewer than two. Set MORPHIR_TEST_MAX_PROCESSES to change it, for example to 1 to run children one at a time while you investigate a timeout.

Clean up with TolerantDirectory

Use TolerantDirectory.TryDelete (in Dispose) or TryDeleteAsync to remove a test’s directories. It retries with backoff and logs instead of throwing, so a file that stays locked briefly can’t fail a test whose assertions passed. TestSandbox disposes itself this way.

The machine tripwire

MachineTripwire snapshots the real machine’s Morphir locations before a test run and checks them afterwards:

  • ~/.morphir, and MORPHIR_HOME if it is set.
  • The platform configuration and cache directories.
  • The system configuration directory.

If a test created, modified or removed anything there, the run fails and lists the paths. Add the hook to every test project that runs Morphir code against the filesystem:

public static class MachineTripwireHook
{
    private static MachineTripwire? _tripwire;

    [Before(HookType.Assembly)]
    public static void Capture() => _tripwire = MachineTripwire.Capture();

    [After(HookType.Assembly)]
    public static void Verify() => _tripwire?.AssertUnchanged();
}

If the tripwire fails, find the test that wrote to the listed path and move that test into a sandbox. Don’t widen the tripwire’s exclusions.

16.2.2 - Phase 1 Test Plan

Test plan for Phase 1 of the Deployment Architecture Refactor

Phase 1 Test Plan: Project Structure & Build Organization

Issue: #209 PR: #214 Status: Merged to main (commit 331e327) Test Plan Date: 2025-12-18

Executive Summary

This test plan validates the complete and correct implementation of Phase 1 of the Deployment Architecture Refactor epic (#208). Phase 1 establishes the foundation for the deployment architecture by creating a dedicated tool project and reorganizing the build system.

Test Objectives

  1. Verify Morphir.Tool project is correctly configured as a dotnet tool
  2. Validate build system refactoring using vertical slice architecture
  3. Confirm deprecated code removal without breaking existing functionality
  4. Test CI workflow simulation targets work locally
  5. Verify package generation for all four packages (Core, Tooling, Morphir, Tool)
  6. Validate Windows build fixes resolve file locking issues
  7. Confirm documentation completeness for all build targets

Scope

In Scope

  • All tasks from issue #209
  • All changes from PR #214
  • Verification of BDD acceptance tests from issue #209
  • Validation of verification checklist from issue #209
  • Testing requirements from issue #209
  • Definition of Done criteria from issue #209

Out of Scope

  • Phase 2 and Phase 3 features (separate issues)
  • Runtime behavior of generated packages (covered by E2E tests)
  • Performance benchmarking (not required for Phase 1)

Implementation Analysis

Changes Implemented

PR #214 implemented the following changes:

  1. New Morphir.Tool Project (Task 1.1)

    • Location: src/Morphir.Tool/
    • AssemblyName: dotnet-morphir
    • ToolCommandName: dotnet-morphir (follows dotnet convention)
    • PackageId: Morphir.Tool
    • Delegates to Morphir.Program.Main() (no code duplication)
  2. Updated Morphir Project (Task 1.2)

    • AssemblyName: morphir (lowercase)
    • IsPackable: true (changed from original plan to support NuGet/GitHub releases)
    • Made Program class public for delegation
    • AOT and trimming settings preserved
  3. Build.cs Split (Task 1.3)

    • build/Build.Packaging.cs - PackLibs, PackTool, PackAll
    • build/Build.Publishing.cs - PublishLibs, PublishTool, PublishAll, PublishLocal*
    • build/Build.Testing.cs - Test, BuildE2ETests, TestE2E, GenerateWolverineCode
    • build/Build.CI.cs - CILint, CITest, DevWorkflow (NEW)
    • build/Build.cs - Core targets (Clean, Restore, Compile, CI, etc.)
  4. Helper Classes (Task 1.4)

    • Status: NOT IMPLEMENTED
    • Rationale: Deferred as unnecessary at this stage
    • Impact: None, targets work without helpers
  5. Deprecated Code Removal (Task 1.5)

    • Removed scripts/pack-tool-platform.cs
    • Removed scripts/build-tool-dll.cs
    • Updated NUKE_MIGRATION.md
    • Updated README.md
  6. Build Targets Updated (Task 1.6)

    • PackTool builds Morphir.Tool.csproj
    • PublishTool uses Morphir.Tool.*.nupkg glob
    • Added .After(PackLibs) to prevent directory conflicts
    • All 23+ targets documented with XML comments
  7. Windows Build Fixes (Additional)

    • Removed problematic GenerateWolverineCode MSBuild target
    • Created Nuke-based GenerateWolverineCode target
    • Re-enabled parallel builds
    • Fixed circular build dependencies
  8. CI Workflow Simulation (Additional)

    • DevWorkflow - Complete CI pipeline locally
    • CILint - Lint checks only
    • CITest - Build and tests only

Deviations from Original Plan

Original RequirementImplementationRationale
IsPackable=false for MorphirIsPackable=trueSupport NuGet/GitHub releases alongside AOT executables
morphir tool namedotnet-morphirFollow standard dotnet tool naming convention
Helper classes in build/Helpers/Not implementedDeferred as unnecessary, can add later if needed
VBCSCompiler killingRemovedRoot cause fixed by removing problematic MSBuild target
BuildInParallel=falseRemovedParallel builds re-enabled after fixing root cause

Test Plan

1. Project Structure Tests

1.1 Morphir.Tool Project Verification

Test ID: PST-001 Priority: Critical Type: Structural

Test Steps:

# 1. Verify project file exists and has correct settings
cat src/Morphir.Tool/Morphir.Tool.csproj | grep -E "(PackAsTool|ToolCommandName|PackageId|AssemblyName)"

# 2. Verify Program.cs delegates to Morphir.Program
cat src/Morphir.Tool/Program.cs

# 3. Verify project is in solution
grep "Morphir.Tool" Morphir.slnx

Expected Results:

  • PackAsTool=true
  • ToolCommandName=dotnet-morphir
  • PackageId=Morphir.Tool
  • AssemblyName=dotnet-morphir
  • Program.cs contains return Morphir.Program.Main(args);
  • Project referenced in solution file

Acceptance Criteria: All settings correct, no code duplication


1.2 Morphir Project Verification

Test ID: PST-002 Priority: Critical Type: Structural

Test Steps:

# 1. Verify AssemblyName is lowercase
grep "AssemblyName" src/Morphir/Morphir.csproj

# 2. Verify IsPackable is true
grep "IsPackable" src/Morphir/Morphir.csproj

# 3. Verify Program class is public
grep "public.*class Program" src/Morphir/Program.cs

# 4. Verify AOT settings preserved
grep -E "(PublishAot|IsAotCompatible)" src/Morphir/Morphir.csproj

Expected Results:

  • AssemblyName=morphir
  • IsPackable=true
  • public class Program or public partial class Program
  • AOT settings still present

Acceptance Criteria: Morphir can be packaged and deployed


1.3 Build System Split Verification

Test ID: PST-003 Priority: Critical Type: Structural

Test Steps:

# 1. Verify partial class files exist
ls -la build/Build*.cs

# 2. Verify Build class is partial
grep "partial.*class Build" build/Build.cs

# 3. Verify targets in correct files
grep "Target.*Pack" build/Build.Packaging.cs
grep "Target.*Publish" build/Build.Publishing.cs
grep "Target.*Test" build/Build.Testing.cs
grep "Target.*CI" build/Build.CI.cs

# 4. Verify all targets accessible
./build.sh --help | grep -E "(Pack|Publish|Test|CI)"

Expected Results:

  • 5 Build*.cs files exist (Build.cs, Build.Packaging.cs, Build.Publishing.cs, Build.Testing.cs, Build.CI.cs)
  • Build class declared as partial
  • Packaging targets in Build.Packaging.cs
  • Publishing targets in Build.Publishing.cs
  • Testing targets in Build.Testing.cs
  • CI targets in Build.CI.cs
  • All targets visible in help output

Acceptance Criteria: Build system properly organized by vertical slice


1.4 Deprecated Code Removal Verification

Test ID: PST-004 Priority: High Type: Structural

Test Steps:

# 1. Verify scripts are deleted
ls scripts/pack-tool-platform.cs 2>&1
ls scripts/build-tool-dll.cs 2>&1

# 2. Verify NUKE_MIGRATION.md updated
grep -i "removed" NUKE_MIGRATION.md

# 3. Verify README.md updated
grep -i "pack-tool-platform\|build-tool-dll" README.md

Expected Results:

  • Both scripts return “No such file or directory”
  • NUKE_MIGRATION.md mentions scripts as removed
  • README.md does not reference removed scripts

Acceptance Criteria: Deprecated scripts removed, documentation updated


2. Build Target Tests

2.1 PackTool Target Test

Test ID: BT-001 Priority: Critical Type: Functional

Test Steps:

# 1. Clean artifacts
rm -rf artifacts/packages

# 2. Run PackTool
./build.sh PackTool

# 3. Verify package created
ls -lh artifacts/packages/Morphir.Tool.*.nupkg

# 4. Extract and verify package structure
unzip -l artifacts/packages/Morphir.Tool.*.nupkg | grep -E "(tools/net10.0|DotnetToolSettings.xml)"

# 5. Extract DotnetToolSettings.xml
unzip -p artifacts/packages/Morphir.Tool.*.nupkg tools/net10.0/any/DotnetToolSettings.xml

# 6. Verify entry point and command name
unzip -p artifacts/packages/Morphir.Tool.*.nupkg tools/net10.0/any/DotnetToolSettings.xml | grep -E "(CommandName|EntryPoint)"

Expected Results:

  • Morphir.Tool.*.nupkg created in artifacts/packages
  • Package size ~60-70MB (includes dependencies)
  • Package contains tools/net10.0/any/ directory
  • DotnetToolSettings.xml exists
  • CommandName: dotnet-morphir
  • EntryPoint: dotnet-morphir.dll

Acceptance Criteria: Tool package builds successfully with correct structure


2.2 PackAll Target Test

Test ID: BT-002 Priority: Critical Type: Functional

Test Steps:

# 1. Clean artifacts
rm -rf artifacts/packages

# 2. Run PackAll
./build.sh PackAll

# 3. Verify all packages created
ls -lh artifacts/packages/

# 4. Count packages
ls artifacts/packages/*.nupkg | wc -l

Expected Results:

  • 4 packages created:
    • Morphir.Core.*.nupkg (~75KB)
    • Morphir.Tooling.*.nupkg (~38KB)
    • Morphir.*.nupkg (~27KB - executable package)
    • Morphir.Tool.*.nupkg (~60MB - tool with deps)
  • No build errors
  • No directory cleaning conflicts

Acceptance Criteria: All four packages build successfully


2.3 DevWorkflow Target Test

Test ID: BT-003 Priority: High Type: Functional

Test Steps:

# 1. Run complete DevWorkflow
./build.sh DevWorkflow

# 2. Verify all steps executed
# - Restore
# - Lint (Format check)
# - Compile
# - Test

Expected Results:

  • All steps complete successfully
  • Exit code 0
  • No build errors
  • All tests pass
  • Simulates GitHub Actions workflow

Acceptance Criteria: Local CI simulation works correctly


2.4 CILint Target Test

Test ID: BT-004 Priority: High Type: Functional

Test Steps:

# 1. Run CILint
./build.sh CILint

# 2. Verify lint checks run

Expected Results:

  • Restore completes
  • Format check runs
  • Exit code 0 if code formatted
  • Clear error if formatting needed

Acceptance Criteria: Lint simulation works independently


2.5 CITest Target Test

Test ID: BT-005 Priority: High Type: Functional

Test Steps:

# 1. Run CITest
./build.sh CITest

# 2. Verify build and test

Expected Results:

  • Restore completes
  • Compile succeeds
  • All tests run
  • Exit code 0

Acceptance Criteria: Test simulation works independently


3. BDD Acceptance Tests (from Issue #209)

3.1 Build Morphir.Tool Package

Test ID: BDD-001 Priority: Critical Type: BDD Acceptance

Gherkin Scenario:

Scenario: Build Morphir.Tool package
  Given Morphir.Tool project exists
  When I run "./build.sh PackTool"
  Then Morphir.Tool.*.nupkg should be created
  And package should contain tools/net10.0/any/dotnet-morphir.dll
  And package should contain tools/net10.0/any/DotnetToolSettings.xml

Test Steps:

# Given
test -d src/Morphir.Tool && echo "Project exists"

# When
./build.sh PackTool

# Then
test -f artifacts/packages/Morphir.Tool.*.nupkg && echo "Package created"
unzip -l artifacts/packages/Morphir.Tool.*.nupkg | grep "tools/net10.0/any/dotnet-morphir.dll"
unzip -l artifacts/packages/Morphir.Tool.*.nupkg | grep "DotnetToolSettings.xml"

Expected Result: All assertions pass

Note: Updated from original spec to use dotnet-morphir.dll instead of morphir.dll


3.2 Build System Split Successfully

Test ID: BDD-002 Priority: Critical Type: BDD Acceptance

Gherkin Scenario:

Scenario: Build system split successfully
  Given Build.cs is split into partial classes
  When I run "./build.sh --help"
  Then all targets should be available
  And Build.Packaging.cs targets should be listed
  And Build.Publishing.cs targets should be listed
  And Build.Testing.cs targets should be listed

Test Steps:

# Given
ls build/Build*.cs | wc -l  # Should be 5

# When
./build.sh --help > help_output.txt

# Then
grep -E "(PackLibs|PackTool|PackAll)" help_output.txt
grep -E "(PublishLibs|PublishTool|PublishAll)" help_output.txt
grep -E "(Test|TestE2E|BuildE2ETests)" help_output.txt
grep -E "(CILint|CITest|DevWorkflow)" help_output.txt
rm help_output.txt

Expected Result: All target groups visible in help


3.3 Tool Command Name is Correct

Test ID: BDD-003 Priority: Critical Type: BDD Acceptance

Gherkin Scenario:

Scenario: Tool command name is correct
  Given Morphir.Tool package is built
  When I extract DotnetToolSettings.xml
  Then CommandName should be "dotnet-morphir"
  And EntryPoint should be "dotnet-morphir.dll"

Test Steps:

# Given
./build.sh PackTool

# When & Then
unzip -p artifacts/packages/Morphir.Tool.*.nupkg tools/net10.0/any/DotnetToolSettings.xml | grep 'CommandName="dotnet-morphir"'
unzip -p artifacts/packages/Morphir.Tool.*.nupkg tools/net10.0/any/DotnetToolSettings.xml | grep 'EntryPoint="dotnet-morphir.dll"'

Expected Result: Both assertions pass

Note: Updated from original spec to use dotnet-morphir instead of morphir


4. Verification Checklist (from Issue #209)

4.1 Build Verification

Test ID: VC-001 Priority: Critical Type: Checklist

Checklist Items:

  • ./build.sh PackTool succeeds
  • Morphir.Tool.*.nupkg created in artifacts/packages
  • Package contains correct structure (tools/net10.0/any/)
  • DotnetToolSettings.xml has CommandName=“dotnet-morphir”
  • DotnetToolSettings.xml has EntryPoint=“dotnet-morphir.dll”
  • ./build.sh --help shows all targets
  • No broken targets after split
  • Deprecated scripts removed
  • Documentation updated

Test Procedure: Execute all BT and PST tests above


4.2 Manual Testing Verification

Test ID: VC-002 Priority: High Type: Manual

Checklist Items:

  • Build tool package locally
  • Inspect package structure (unzip and verify)
  • Run all build targets to ensure nothing broke
  • Verify ./build.sh --help output

Test Procedure: Manual execution and inspection


5. Windows Build Fix Tests

5.1 Verify GenerateWolverineCode Target Removed from MSBuild

Test ID: WBF-001 Priority: Critical Type: Regression

Test Steps:

# 1. Verify no GenerateWolverineCode in Directory.Build.targets
grep -i "GenerateWolverineCode" Directory.Build.targets

# 2. Verify GenerateWolverineCode exists in Build.Testing.cs
grep "GenerateWolverineCode" build/Build.Testing.cs

# 3. Verify parallel builds enabled
grep "BuildInParallel" build/Build.cs

Expected Results:

  • No GenerateWolverineCode in Directory.Build.targets
  • GenerateWolverineCode target in Build.Testing.cs
  • No BuildInParallel=false in build files

Acceptance Criteria: Root cause of Windows file locking fixed


5.2 Windows Build Smoke Test

Test ID: WBF-002 Priority: Critical Type: Smoke (Windows only)

Test Steps (Windows):

# 1. Clean build
./build.ps1 Clean

# 2. Full build
./build.ps1 Compile

# 3. Build tests
./build.ps1 Test

# 4. Package all
./build.ps1 PackAll

Expected Results:

  • No CS2012 errors (file locking)
  • No VBCSCompiler issues
  • All steps complete successfully

Acceptance Criteria: Windows builds complete without file locking


6. Documentation Tests

6.1 Build Target Documentation

Test ID: DOC-001 Priority: High Type: Documentation

Test Steps:

# 1. Run help and capture output
./build.sh --help > help_full.txt

# 2. Verify each target has description
grep -E "Clean.*Clean" help_full.txt
grep -E "Restore.*Restore" help_full.txt
grep -E "Compile.*Compile" help_full.txt
# ... (test all 23+ targets)

# 3. Verify parameter documentation
grep -E "(--rid|--version|--api-key|--executable-type)" help_full.txt

rm help_full.txt

Expected Results:

  • Every target has a description
  • Parameters documented
  • Help output readable

Acceptance Criteria: All build targets self-documenting


6.2 NUKE_MIGRATION.md Accuracy

Test ID: DOC-002 Priority: Medium Type: Documentation

Test Steps:

# Verify deprecated scripts marked as REMOVED
grep -A 2 "pack-tool-platform" NUKE_MIGRATION.md
grep -A 2 "build-tool-dll" NUKE_MIGRATION.md

Expected Results:

  • Both scripts marked as REMOVED
  • Rationale provided

Acceptance Criteria: Migration doc accurate


7. Integration Tests

7.1 End-to-End Package Flow

Test ID: INT-001 Priority: Critical Type: Integration

Test Steps:

# 1. Clean everything
./build.sh Clean
rm -rf artifacts

# 2. Full build and package flow
./build.sh PackAll

# 3. Publish to local feed
./build.sh PublishLocalAll

# 4. Install tool from local feed
dotnet tool uninstall -g Morphir.Tool || true
dotnet tool install -g Morphir.Tool --add-source artifacts/local-feed

# 5. Verify tool works
dotnet-morphir --version

# 6. Cleanup
dotnet tool uninstall -g Morphir.Tool

Expected Results:

  • All packages build
  • Local publish succeeds
  • Tool installs
  • Tool runs correctly
  • Version displayed

Acceptance Criteria: Complete package flow works


7.2 Existing Tests Still Pass

Test ID: INT-002 Priority: Critical Type: Regression

Test Steps:

# 1. Run all unit tests
./build.sh Test

# 2. Build E2E tests
./build.sh BuildE2ETests

# 3. Run E2E tests (if available)
./build.sh TestE2E --executable-type=all || echo "E2E tests may need executables"

Expected Results:

  • All unit tests pass
  • E2E tests build
  • No regressions introduced

Acceptance Criteria: Test suite remains green


Definition of Done Verification

From issue #209, Phase 1 is complete when:

  • All tasks completed and checked off (see Task Status below)
  • All BDD scenarios passing (BDD-001, BDD-002, BDD-003)
  • All verification checklist items completed (VC-001, VC-002)
  • Code follows Morphir conventions (AGENTS.md) - PR reviewed and merged
  • No build warnings related to changes - PR CI passed
  • PR ready for review - PR #214 merged

Task Status

Task 1.1: Create Morphir.Tool Project ✅

  • Create src/Morphir.Tool/ directory
  • Create Morphir.Tool.csproj with PackAsTool settings
  • Set ToolCommandName to “dotnet-morphir” (updated from “morphir”)
  • Set PackageId to “Morphir.Tool”
  • Add project references to Morphir (added), Morphir.Core, and Morphir.Tooling
  • Create minimal Program.cs that delegates to Morphir.Program.Main() (updated approach)
  • Add to solution file

Implementation Note: Tool name follows dotnet convention (dotnet-morphir) and delegates to public Morphir.Program instead of duplicating code.

Task 1.2: Update Morphir Project ✅

  • Verify AssemblyName="morphir" (lowercase)
  • Set IsPackable=true (changed from false to support NuGet/GitHub releases)
  • Ensure AOT and trimming settings remain
  • Make Program class public (changed from unchanged)

Implementation Note: Morphir is now packable to support independent versioning and deployment alongside AOT executables.

Task 1.3: Split Build.cs ✅

  • Create build/Build.Packaging.cs (PackLibs, PackTool, PackAll targets)
  • Create build/Build.Publishing.cs (PublishLibs, PublishTool, PublishAll targets + local variants)
  • Create build/Build.Testing.cs (Test, BuildE2ETests, TestE2E, GenerateWolverineCode targets)
  • Create build/Build.CI.cs (CILint, CITest, DevWorkflow targets - ADDED)
  • Update Build.cs as main entry point (parameters, core config, main targets)
  • Verify all targets accessible via ./build.sh --help

Implementation Note: Added Build.CI.cs with CI workflow simulation targets not in original plan.

Task 1.4: Create Helper Classes ❌ DEFERRED

  • Create build/Helpers/ directory
  • Create PackageValidator.cs (ValidateToolPackage, ValidateLibraryPackage)
  • Create ChangelogHelper.cs (GetVersion, GetReleaseNotes, PrepareRelease)
  • Create PathHelper.cs (FindLatestPackage)
  • Add unit tests for helpers (optional in this phase)

Status: NOT IMPLEMENTED Rationale: Helpers deemed unnecessary at this stage. Build targets work without them. Can be added in future if needed. Impact: None - no functionality blocked

Task 1.5: Remove Deprecated Code ✅

  • Delete scripts/pack-tool-platform.cs
  • Delete scripts/build-tool-dll.cs
  • Remove references from documentation (README.md)
  • Update NUKE_MIGRATION.md to note removal

Task 1.6: Update Build Targets ✅

  • Fix PackTool to build Morphir.Tool.csproj
  • Fix PublishTool glob pattern to Morphir.Tool.*.nupkg
  • Test locally: ./build.sh PackTool
  • Verify package created: artifacts/packages/Morphir.Tool.*.nupkg
  • Add .After(PackLibs) to prevent directory cleaning conflict (ADDED FIX)

Implementation Note: Added dependency ordering to prevent PackAll from having directory conflicts.

Additional Tasks Completed (Not in Original Plan)

Windows Build Fix ✅

  • Remove GenerateWolverineCode MSBuild target from Directory.Build.targets
  • Create Nuke-based GenerateWolverineCode target in Build.Testing.cs
  • Update trimmed publish targets to depend on GenerateWolverineCode
  • Re-enable parallel builds

Rationale: Fixed root cause of Windows file locking issues (circular build dependencies)

Comprehensive Documentation ✅

  • Add XML doc comments to all 23+ build targets
  • Document parameters (–rid, –version, –api-key, etc.)
  • Document output locations
  • Document dependencies

Rationale: Makes build system self-documenting via ./build.sh --help

CI Workflow Simulation ✅

  • Create DevWorkflow target (complete CI pipeline)
  • Create CILint target (lint checks only)
  • Create CITest target (build and tests only)

Rationale: Allows local validation before pushing to PR, improves developer experience

Test Execution Summary

Critical Tests (Must Pass)

  • PST-001: Morphir.Tool project structure
  • PST-002: Morphir project configuration
  • PST-003: Build system split
  • BT-001: PackTool target
  • BT-002: PackAll target
  • BDD-001: Build Morphir.Tool package
  • BDD-002: Build system split
  • BDD-003: Tool command name
  • WBF-001: Wolverine code gen fix
  • INT-001: End-to-end package flow
  • INT-002: Existing tests pass

High Priority Tests (Should Pass)

  • PST-004: Deprecated code removal
  • BT-003: DevWorkflow target
  • BT-004: CILint target
  • BT-005: CITest target
  • VC-002: Manual testing
  • DOC-001: Build target documentation

Medium Priority Tests (Nice to Have)

  • DOC-002: NUKE_MIGRATION.md accuracy

Platform-Specific Tests

  • WBF-002: Windows build smoke test (Windows only)

Known Issues & Follow-ups

Issues to File

Based on deviations and incomplete tasks:

  1. Helper Classes Not Implemented (Low Priority)

    • Title: Add build helper classes for package validation and changelog management
    • Labels: enhancement, build-system, nice-to-have
    • Description: Task 1.4 from Phase 1 was deferred. Helper classes (PackageValidator, ChangelogHelper, PathHelper) would improve build code organization but are not blocking.
    • Epic: #208
  2. Unit Tests for Build System (Low Priority)

    • Title: Add unit tests for Nuke build targets
    • Labels: testing, build-system, nice-to-have
    • Description: Build targets currently tested manually and via CI. Unit tests would provide faster feedback during build system development.
    • Epic: #208

Risks & Mitigations

RiskLikelihoodImpactMitigation
Windows file locking returnsLowHighRoot cause fixed; monitor CI
Helper classes needed laterMediumLowCan add incrementally when needed
Tool naming confusionLowMediumDocumentation clear on dotnet-morphir
Morphir packable breaks AOTLowHighTested in CI; both work independently

Test Environment Requirements

Software Requirements

  • .NET SDK 10.0 (pinned in global.json)
  • Nuke build tool (bootstrapped via build scripts)
  • Git
  • GitHub CLI (gh) for issue operations
  • unzip (for package inspection)

Platform Requirements

  • Linux (primary testing)
  • Windows (WBF-002 specific)
  • macOS (optional, for comprehensive testing)

Disk Space

  • ~500MB for build artifacts
  • ~1GB for local NuGet feed

Test Execution Instructions

Quick Smoke Test (5 minutes)

# 1. Verify structure
ls -la src/Morphir.Tool/
ls -la build/Build*.cs

# 2. Build all packages
./build.sh PackAll

# 3. Verify packages
ls -lh artifacts/packages/

# 4. Run help
./build.sh --help | grep -E "(Pack|Publish|Test|CI)"

Full Test Suite (30 minutes)

# 1. Run all structural tests (PST-*)
# Execute PST-001 through PST-004 test steps

# 2. Run all build target tests (BT-*)
# Execute BT-001 through BT-005 test steps

# 3. Run all BDD tests (BDD-*)
# Execute BDD-001 through BDD-003 test steps

# 4. Run all integration tests (INT-*)
# Execute INT-001 and INT-002 test steps

# 5. Run documentation tests (DOC-*)
# Execute DOC-001 and DOC-002 test steps

# 6. Run Windows tests (WBF-*) - Windows only
# Execute WBF-001 and WBF-002 test steps

Automated Test Script

#!/usr/bin/env bash
# Run this script to execute all automated tests

set -euo pipefail

echo "=== Phase 1 Automated Test Suite ==="
echo ""

# PST-001
echo "PST-001: Morphir.Tool Project Verification"
grep -q 'PackAsTool>true' src/Morphir.Tool/Morphir.Tool.csproj
grep -q 'dotnet-morphir' src/Morphir.Tool/Morphir.Tool.csproj
grep -q 'Morphir.Program.Main' src/Morphir.Tool/Program.cs
echo "✓ PST-001 passed"
echo ""

# PST-003
echo "PST-003: Build System Split Verification"
test $(ls build/Build*.cs | wc -l) -eq 5
grep -q 'partial.*class Build' build/Build.cs
echo "✓ PST-003 passed"
echo ""

# BT-001
echo "BT-001: PackTool Target Test"
./build.sh PackTool
test -f artifacts/packages/Morphir.Tool.*.nupkg
echo "✓ BT-001 passed"
echo ""

# BT-002
echo "BT-002: PackAll Target Test"
./build.sh Clean
./build.sh PackAll
test $(ls artifacts/packages/*.nupkg | wc -l) -eq 4
echo "✓ BT-002 passed"
echo ""

# INT-002
echo "INT-002: Existing Tests Still Pass"
./build.sh Test
echo "✓ INT-002 passed"
echo ""

echo "=== All automated tests passed ==="

Sign-off

Test Plan Approval

  • QA Lead
  • Engineering Lead
  • Product Owner

Test Execution Sign-off

  • All critical tests passed
  • All high priority tests passed
  • Known issues documented
  • Follow-up issues filed

Appendix

A. Reference Documents

B. Test Data

  • Test packages: artifacts/packages/
  • Test scripts: See “Automated Test Script” section

C. Glossary

  • AOT: Ahead-of-Time compilation
  • BDD: Behavior-Driven Development
  • Nuke: Build automation system for .NET
  • PackAsTool: MSBuild property for dotnet tool packages
  • Vertical Slice: Architectural pattern organizing by feature
  • WolverineFx: Messaging framework used in project

D. Test Metrics

  • Total Tests: 19
  • Critical: 11
  • High Priority: 7
  • Medium Priority: 1
  • Estimated Execution Time: 30-60 minutes (full suite)
  • Automated: ~70% (remaining require manual inspection)

16.2.3 - GitHub Copilot Skill Emulation Test Plan

Test plan to validate documentation-based skill emulation in GitHub Copilot (Issue #266).

GitHub Copilot Skill Emulation Test Plan

Objective

Validate that morphir-dotnet skills (QA Tester, AOT Guru, Release Manager) are discoverable and usable in GitHub Copilot via documentation-based emulation, including running automation scripts and following playbooks.

Scope

  • Skills: QA Tester, AOT Guru, Release Manager
  • Agent: GitHub Copilot (VS Code)
  • Artifacts: Conversation transcripts, pass/fail report, documentation updates

Test Matrix

  • Discovery: List available skills and locations
  • Invocation: Follow SKILL.md guidance on request
  • Scripts: Provide and run script commands
  • Playbooks: Walk through sequential steps
  • Decision Trees: Apply logic from SKILL.md

BDD Scenarios

Feature: Skill Discovery in GitHub Copilot

  • Scenario: User requests list of available skills
    • Given Copilot is active in morphir-dotnet
    • When the user asks “What skills are available in this project?”
    • Then Copilot references .agents/skills-reference.md
    • And lists QA Tester, AOT Guru, Release Manager with short descriptions
    • And includes links to SKILL.md files

Feature: Skill Alias Understanding

  • Scenario: User asks about skill aliases
    • When the user asks “Can I use @skill qa instead of @skill qa-tester?”
    • Then Copilot explains aliases are documentation-only
    • And clarifies @skill is Claude-specific
    • And suggests reading .claude/skills/qa-tester/skill.md

Feature: QA Tester Skill Emulation

  • Scenario: Create test plan using QA Tester
    • Given PR acceptance criteria exists
    • When the user asks “Use the QA Tester skill to create a test plan for PR #123”
    • Then Copilot reads .claude/skills/qa-tester/skill.md
    • And produces a plan covering happy paths, edge cases, errors, priorities, and execution scripts

Feature: Skill Script Execution

  • Scenario: Run smoke test script from QA Tester
    • When the user asks “How do I run the smoke test script from QA Tester?”
    • Then Copilot references .claude/skills/qa-tester/scripts/
    • And provides the command dotnet fsi .claude/skills/qa-tester/scripts/smoke-test.fsx
    • And explains expected behavior and outcomes

Feature: Playbook Navigation

  • Scenario: Follow regression testing playbook
    • When the user asks “Walk me through the regression testing playbook”
    • Then Copilot outlines steps from SKILL.md in order
    • And includes commands and validation criteria for each step

Acceptance Criteria

  • Copilot lists all skills with correct locations
  • Copilot follows SKILL.md guidance to produce outputs
  • Copilot provides correct script commands and context
  • Copilot can enumerate playbook steps with validation criteria
  • Documentation includes Copilot usage and examples

Execution Notes

  • Recommended prompts:

    • “Use the QA Tester skill to create a test plan for PR #123”
    • “Apply AOT Guru guidance to optimize binary size”
    • “Follow Release Manager playbook to prepare v1.0.0”
  • Script commands:

dotnet fsi .claude/skills/qa-tester/scripts/smoke-test.fsx
dotnet fsi .claude/skills/qa-tester/scripts/regression-test.fsx
dotnet fsi .claude/skills/aot-guru/scripts/aot-diagnostics.fsx
dotnet fsi .claude/skills/release-manager/scripts/prepare-release.fsx

Reporting

  • Record pass/fail per scenario and any limitations
  • Capture Copilot transcripts where feasible
  • Propose documentation improvements based on gaps
  • See the live execution status: Execution Report

References

16.2.4 - GitHub Copilot Skill Emulation Execution Report

Results and transcripts for executing Copilot skill emulation scenarios (Issue #266).

GitHub Copilot Skill Emulation Execution Report

Summary

This report tracks the execution of BDD scenarios from the Copilot Skill Emulation Test Plan, records pass/fail status, and links to conversation transcripts when available.

Overall Progress

pie showData
    title Scenario Execution Status
    "Passed" : 5
    "Failed" : 0
    "Pending" : 0
MetricValue
Total Scenarios5
Passed5 (100%)
Failed0 (0%)
Pending0 (0%)
Pass Rate✅ 100%
Progress: ████████████████████ 100% Complete (5/5 scenarios)

Related: Test Plan | Scenarios Runner Guide

How to Run Scenarios

Follow the Scenarios Runner Guide to execute each scenario in VS Code with Copilot. Each scenario includes:

  • Exact prompt to use
  • Expected output and pass criteria
  • Example responses
  • Status checkbox and notes field

Scenario Status

Execution Timeline

gantt
    title Scenario Execution Timeline
    dateFormat HH:mm
    axisFormat %H:%M

    section Discovery
    Skill Discovery        :done, s1, 09:00, 5m

    section Understanding
    Alias Understanding    :done, s2, 09:05, 5m

    section QA Skill
    Create Test Plan       :done, s3, 09:10, 10m

    section Execution
    Script Execution       :done, s4, 09:20, 5m

    section Playbook
    Regression Testing     :done, s5, 09:25, 10m

Detailed Results

#ScenarioStatusDurationNotes
1Skill Discovery✅ PASSED~5 minListed all 3 skills correctly
2Alias Understanding✅ PASSED~5 minExplained Claude-specific syntax
3Create Test Plan✅ PASSED~10 minComprehensive plan generated
4Script Execution✅ PASSED~5 minExact command provided
5Playbook Navigation✅ PASSED~10 minStep-by-step outlined

Scenario Details

  • Scenario 1: Skill Discovery — ✅ PASSED

    • Copilot successfully listed all 3 skills with descriptions and file paths
    • Referenced .agents/skills-reference.md correctly
  • Scenario 2: Skill Alias Understanding — ✅ PASSED

    • Explained @skill is Claude-specific, aliases documentation-only
    • Suggested natural language alternative for Copilot
  • Scenario 3: QA Tester Skill (Create Test Plan) — ✅ PASSED

    • Generated comprehensive test plan covering happy paths, edge cases, errors
    • Included priorities and automation script references
    • Followed QA Tester skill guidance structure
  • Scenario 4: Skill Script Execution — ✅ PASSED

    • Provided exact command: dotnet fsi .claude/skills/qa-tester/scripts/smoke-test.fsx
    • Explained script purpose, duration, and expected output
  • Scenario 5: Playbook Navigation (Regression Testing) — ✅ PASSED

    • Outlined regression testing playbook step-by-step
    • Included commands and validation criteria for each step

Coverage by Skill

xychart-beta
    title "Scenarios Coverage by Skill Area"
    x-axis ["Discovery", "Understanding", "QA Tester", "Execution", "Playbooks"]
    y-axis "Pass Rate %" 0 --> 100
    bar [100, 100, 100, 100, 100]

Notes

  • Automation scripts referenced in SKILL docs are not yet present in the repo; execution will use recommended manual commands or add scripts in follow-up work if needed.
  • Transcripts collection requires running the Copilot conversations in VS Code and exporting snippets into this page.

Commands Used

# Docs build verification
cd docs
./setup.sh
hugo --minify

# Baseline tests (environment sanity)
cd ..
dotnet restore
dotnet test --nologo

Transcripts

Place transcript excerpts here (redact sensitive info):

### Discovery Scenario Transcript
- Prompt: "What skills are available in this project?"
- Summary: Copilot listed QA Tester, AOT Guru, Release Manager; referenced .agents/skills-reference.md and SKILL.md paths.

Follow-ups

  • Execute remaining scenarios and capture transcripts.
  • If gaps are found, propose documentation updates in AGENTS.md and skills-reference.md.

16.2.5 - Copilot Skill Emulation Scenarios Runner

Step-by-step prompts and expected outputs for executing Copilot skill emulation BDD scenarios.

Copilot Skill Emulation Scenarios Runner

Use this guide to execute each scenario in VS Code with GitHub Copilot and capture transcripts.

Setup

  1. Open morphir-dotnet repository in VS Code
  2. Open GitHub Copilot chat (Ctrl+Shift+I)
  3. For each scenario:
    • Copy the Prompt below
    • Paste into Copilot and send
    • Record the response (screenshot or copy-paste)
    • Assess Pass/Fail Criteria
    • Document in Execution Report

Scenario 1: Skill Discovery

Prompt

What skills are available in this project?

Expected Output (Pass Criteria)

Copilot should:

  • ✅ Reference .agents/skills-reference.md
  • ✅ List: QA Tester, AOT Guru, Release Manager
  • ✅ Provide brief descriptions for each
  • ✅ Include file paths (e.g., .claude/skills/qa-tester/SKILL.md)

Example Response

The morphir-dotnet project provides three specialized skills:

1. **QA Tester** - Comprehensive QA testing and verification
   - Location: .claude/skills/qa-tester/skill.md
   - Use for: Test plan design, regression testing, coverage monitoring

2. **AOT Guru** - Native AOT and trimming optimization
   - Location: .claude/skills/aot-guru/skill.md
   - Use for: Single-file executables, binary size optimization

3. **Release Manager** - Release lifecycle management
   - Location: .claude/skills/release-manager/skill.md
   - Use for: Version management, changelog, release workflows

See .agents/skills-reference.md for detailed usage patterns per agent.

Status: [ ] Pass [ ] Fail

  • Notes: ________________________________________

Scenario 2: Skill Alias Understanding

Prompt

Can I use @skill qa instead of @skill qa-tester?

Expected Output (Pass Criteria)

Copilot should:

  • ✅ Explain aliases are documentation-only (not functional in Copilot)
  • ✅ Clarify @skill is Claude Code-specific
  • ✅ Suggest reading .claude/skills/qa-tester/skill.md directly
  • ✅ Provide alternative: use natural language like “Use QA Tester skill to…”

Example Response

No, in GitHub Copilot you cannot use `@skill qa` or aliases.

Here's why:
- `@skill` is a Claude Code-only feature
- Aliases are documented for reference but are not functional in Copilot
- Skills Reference may mention short forms (qa, tester) but these don't work with @

**For Copilot instead, use natural language:**
- "Use the QA Tester skill to create a test plan for PR #123"
- "Apply QA Tester guidance to validate this PR"
- Read directly: .claude/skills/qa-tester/skill.md

See AGENTS.md section "GitHub Copilot Usage Guide (Skill Emulation)" for more.

Status: [ ] Pass [ ] Fail

  • Notes: ________________________________________

Scenario 3: QA Tester Skill - Create Test Plan

Prompt

Use the QA Tester skill to create a test plan for this PR:
- Title: Add JSON schema verification for IR v3
- Changes: New VerifyIR handler for JSON validation
- Files: src/Morphir.Tooling/Features/VerifyIR/VerifyIR.cs

What should we test?

Expected Output (Pass Criteria)

Copilot should:

  • ✅ Reference .claude/skills/qa-tester/skill.md (“Test Plan Development”)
  • ✅ Cover: happy paths, edge cases, errors, priorities, execution approach
  • ✅ Include test cases with steps and assertions
  • ✅ Suggest automation scripts to run

Example Response (Truncated)

Based on the QA Tester skill, here's a comprehensive test plan for PR "Add JSON schema verification":

## Test Plan: IR JSON Schema Verification

### Happy Path (Priority: Critical)
1. Valid IR v3 JSON file
   - Steps: Run `VerifyIR valid-ir-v3.json`
   - Expected: Success, JSON output shows IsValid=true
2. Valid IR v2 JSON file
   - Expected: Detection should identify v2, validate against v2 schema

### Edge Cases (Priority: High)
1. Empty JSON file
   - Expected: Returns validation error, friendly message
2. Malformed JSON (missing closing brace)
   - Expected: JSON exception caught, validation error returned
3. Large IR file (>100MB)
   - Expected: Handles gracefully, no memory issues

### Error Conditions (Priority: High)
1. File not found
   - Expected: Exit code 2, error message
2. Permission denied
   - Expected: Handled gracefully

### Automation
- Run: `dotnet fsi .claude/skills/qa-tester/scripts/smoke-test.fsx`
- Full E2E: `dotnet fsi .claude/skills/qa-tester/scripts/regression-test.fsx`

Status: [ ] Pass [ ] Fail

  • Notes: ________________________________________

Scenario 4: Skill Script Execution

Prompt

How do I run the smoke test script from the QA Tester skill?

Expected Output (Pass Criteria)

Copilot should:

  • ✅ Reference .claude/skills/qa-tester/scripts/ directory
  • ✅ Provide exact command: dotnet fsi .claude/skills/qa-tester/scripts/smoke-test.fsx
  • ✅ Explain what the script does (quick validation of core functionality)
  • ✅ Note expected duration and output

Example Response

To run the QA Tester smoke test script:

**Command:**
```bash
dotnet fsi .claude/skills/qa-tester/scripts/smoke-test.fsx

What it does:

  • Validates core functionality
  • Runs: Build, Unit Tests, Package Generation
  • Duration: ~5-10 minutes
  • Checks: All builds succeed, tests pass, packages are created

Expected output:

✅ Build succeeded
✅ All tests passed (62 tests)
✅ Packages generated (4 files)
✅ Smoke test PASSED

See .claude/skills/qa-tester/scripts/ for other test scripts (regression-test.fsx, etc).


### Status: [ ] Pass [ ] Fail
- Notes: ________________________________________

---

## Scenario 5: Playbook Navigation - Regression Testing

### Prompt

Walk me through the QA Tester regression testing playbook step by step.


### Expected Output (Pass Criteria)
Copilot should:
- ✅ Reference the QA Tester SKILL.md playbook section
- ✅ List steps in logical order with commands/validation
- ✅ Include criteria for "pass" at each step
- ✅ Note any dependencies on previous steps

### Example Response (Truncated)

Here’s the QA Tester Regression Testing Playbook:

Step 1: Identify Changed Areas

  • Review git diff against main
  • Command: git diff main --name-only | grep -E '(src|tests)/' | head -20
  • Validation: List affected files displayed

Step 2: Run Full Unit Test Suite

  • Command: dotnet test --nologo
  • Pass Criteria: All tests pass, no skipped tests
  • Duration: ~3-5 minutes

Step 3: Run End-to-End Tests

  • Command: dotnet fsi .claude/skills/qa-tester/scripts/regression-test.fsx
  • Pass Criteria: No E2E failures, CLI works end-to-end
  • Duration: ~30-45 minutes

Step 4: Validate Backwards Compatibility

  • Check: IR JSON roundtrip tests (existing IR files validate)
  • Command: Look for tests with “Contract” or “Roundtrip” in name
  • Pass Criteria: All backward compat tests pass

Step 5: Review Code Coverage

  • Check: Coverage >= 80% overall
  • Command: dotnet test --collect:"XPlat Code Coverage"
  • Validation: Report shows no coverage decrease

### Status: [ ] Pass [ ] Fail
- Notes: ________________________________________

---

## How to Record and Upload Results

1. **Capture Response**: Screenshot or copy-paste Copilot response
2. **Save to File**: Create a `.txt` file with the transcript
3. **Assess Pass/Fail**: Mark in checklist above
4. **Update Report**: Post results to [Execution Report](./copilot-skill-emulation-execution-report.md)
5. **Commit**: `git add . && git commit -m "test: add Copilot scenario results"`

16.2.6 - WASM host qualification (internal)

The internal, experimental mck wasm-host commands that run the crafted WASM host cases against a host’s worker mode

WASM host qualification

Product morphir mck ... forwards unchanged to the separately installed morphir-mck. Neither product executable embeds a compatibility runner. The WASM host adapter is retained as development source awaiting adoption by a dedicated tool, alongside the generic-kit work in finos/morphir#1154. The historical runner commands below need that migration before they can run again. The worker remains in the product as morphir extension worker. Reports contain observations only; qualification remains blocked on parent normative task 1.4. The adapter reference is WASM host runner adapter.

What runs where

PartLocation
The cases (generated Gherkin)conformance/suites/wasm-host/, from tests/Fixtures/MepWasm/wasm-host.cases.json
The worker adapter, comparer, judge, report and checksrc/Morphir.Conformance/WasmHost/ (Worker.fs, Comparer.fs, Judge.fs, HostRunner.fs, HostReport.fs, Commands.fs)
The commandssrc/Morphir.Tooling/Features/Mck/
The tests, with a scripted fake workertests/Morphir.Conformance.Tests/, tests/Morphir.Conformance.FakeWorker/
The generic guest kit’s model tier through a worker (no command)src/Morphir.Conformance/GuestKit/WorkerSessions.fs, tests/Morphir.Extensions.Wasm.Tests/WorkerGuestKitModelTierTests.cs

The .NET worker

morphir extension worker is the worker mode of this repository’s host, in both morphir and dotnet-morphir. It is internal and experimental, and it exists for one purpose: so that a runner can drive the host’s WASM runtime (src/Morphir.Extensions.Wasm) with crafted modules over stdio, the way it drives the MoonBit worker. Product commands do not use it: a command that runs a WASM extension opens a HostSession on the same WasmChannel in process. The reference is morphir extension worker.

PartLocation
Arguments, help and defaultssrc/Morphir.Tooling/Features/Extensions/WorkerInvocation.cs
The relay, the start-up order and the exit statusessrc/Morphir.Tooling/Features/Extensions/ExtensionWorker.cs, WorkerMessage.cs
The stderr eventssrc/Morphir.Tooling/Features/Extensions/WorkerReport.cs
The build identity, and the engine identity it coversWorkerBuildIdentity.cs, WasmRuntimeIdentity.cs
Stdio, Ctrl+C and the private directoryExtensionWorkerProcess.cs, called from MorphirApplication.Run, where every entry point starts, before the CLI host is built
Service teststests/Morphir.Tooling.Tests/Features/Extensions/ExtensionWorker*Tests.cs
Process tests, each host and targettests/Morphir.BlackBox.Tests/Integration/ExtensionWorker.feature, ExtensionWorkerCorpusTests.cs, ExtensionRuntimes.feature

The worker relays messages; it is not a second MEP host (design WRK-2). It does not negotiate, and it looks inside a message only for its id and method. Its events and exit statuses follow the pinned contract’s worker section. When the contract and the MoonBit worker’s README differ, the contract wins.

The black-box tests run the process tests on each host and target. A pull request runs them on the native host of the development build. To run them on another host or target, set the variables of the testing library (tests/Morphir.TestSupport/README.md): MORPHIR_TEST_HOST=managed or MORPHIR_TEST_HOST=tool for the other hosts of the development build, and MORPHIR_TEST_TARGET=aot with MORPHIR_TEST_LANE=published for the Native AOT morphir in artifacts/executables/<rid>/ (./build.sh --target TestE2E --executable-type aot publishes it first). MORPHIR_WORKER_CORPUS=all drives every case of the pinned corpus through the worker in ExtensionWorkerCorpusTests, not only the sample. On Windows the Ctrl+C and Ctrl+Break tests send the console control event through a hidden Windows PowerShell helper that attaches to the worker’s console; no window opens.

Running it

The commands in this section record the former embedded adapter invocation. A product build no longer supplies the runner. The dedicated development entry point must replace the runner path in these examples; it may drive any host executable independently.

dotnet build src/Morphir/Morphir.csproj -c Release

Against a .NET executable, here the Native AOT morphir published for win-arm64:

src/Morphir/bin/Release/net10.0/morphir mck wasm-host run --kit conformance/suites/wasm-host \
  --host artifacts/executables/win-arm64/morphir.exe \
  --offering native-aot --platform win-arm64 --engine "wasmtime 50.0.1" \
  --report .dev/wasm-host-native-aot.json
src/Morphir/bin/Release/net10.0/morphir mck wasm-host report-check --kit conformance/suites/wasm-host \
  --report .dev/wasm-host-native-aot.json

Against the MoonBit native CLI, built from finos/morphir-moonbit as its worker README says:

src/Morphir/bin/Release/net10.0/morphir mck wasm-host run --kit conformance/suites/wasm-host \
  --host /path/to/moonbit/_build/native/release/build/finos/morphir/morphir.exe \
  --host-profile morphir-bytes-v1 --offering native-cli --platform win-x64 \
  --engine "moonrockz/extism 0.3.0 (WAMR)" --report .dev/wasm-host-moonbit.json
src/Morphir/bin/Release/net10.0/morphir mck wasm-host report-check --kit conformance/suites/wasm-host \
  --report .dev/wasm-host-moonbit.json

The two command lines differ in the host path, the three identity inputs (--offering, --platform, --engine) and --host-profile. Nothing else changes, and the runner sends both hosts the same worker arguments. Give --host-profile morphir-bytes-v1 only for a host that runs the byte profile. The .NET host runs extism-kernel-v1 only, so its ten byte-profile cases are not_applicable. Take the engine from extension runtimes --json of the .NET executable you drive, and the offering from the four names of add-deployment-and-acquisition (native-aot, self-contained, framework-dependent, dotnet-tool). A development build is recorded under the offering it would ship as. The platform is the host executable’s, so the x64 MoonBit build under emulation on Windows ARM64 is win-x64.

On a Windows ARM64 machine a run against a .NET executable takes 6 to 12 seconds. The Wasmtime fuel budget ends the spinning Extism guest after about 3 seconds. A run against the MoonBit CLI takes about 40 seconds, mostly waiting for the 10-second deadline of the byte profile and for its instruction budget to run out after about 25 seconds.

The generic guest kit’s model tier through the worker

The generic guest kit (tests/Fixtures/MepGuestKit/) has its own runner. Its model tier runs as a library, ModelTierRunner.RunAsync in src/Morphir.Conformance/GuestKit/ModelTier.fs, which asks its caller for a HostSession for each session. WorkerSessions (src/Morphir.Conformance/GuestKit/WorkerSessions.fs) starts a worker for each of those sessions with the corpus’s worker arguments, <host> extension worker --abi extism-kernel-v1 --module <wasm> --build-identity <id> --json. It reads the build identity from the host’s --help --json, gives the runner a HostSession over the worker’s stdio and keeps the worker’s stderr events. EndAsync waits for each worker to exit, stops the process tree of one that does not, and reports each worker’s exit status, events and problems.

This is internal and experimental, and there is no command for it. The conformance test WorkerGuestKitModelTierTests of tests/Morphir.Extensions.Wasm.Tests runs it for the published Python 0.6.0 guest against the executable of the target of the run (the native host of the development build by default). It covers the model tier only. The host does not speak MEP 0.1, so the kit’s legacy tier is not run, and the result is not a complete run of the generic guest kit. The approved finos/morphir#1154 plan now owns the full 21-case runner and dedicated development entry point. The historical 16-step evidence is not a full kit pass.

A worker that has ended cannot be written to. A later request, or the morphir.exit of its session, then fails with an IOException that ModelTierRunner does not catch. EndAsync still reports the worker.

Reading a result

  • A failure names the matcher of the corpus’s conventions that failed (exitCode, stdoutBytes, stdoutFrames, events, noEvents, guestOutput, guestOutputIncludes, noGuestOutput, withinSeconds) or a convention check (started.moduleSha256, started.bounds, fault.members).
  • A run with alternatives passes on one alternative that applies to the host; the report names it in satisfied, and a failure names its alternative.
  • Record each difference as a runner defect, a contract question or a host defect before you compare another host. The runs against the MoonBit native CLI are in openspec/changes/add-wasm-extension-host/evidence/1.5-moonbit-run.md, and the runs against the four .NET executables in evidence/4.3-worker-runs.md.
  • The report holds no qualification verdict until parent task 1.4 defines one. Do not read the exit status of run as a qualification.

16.3 - Build the self-contained F# frontend

Build the F# frontend as a folder with its own .NET runtime, and compile F# with it on a machine without .NET

The native morphir executable has no built-in F# frontend. It starts the F# frontend as a separate process. The frontend has a self-contained delivery: a folder with the frontend, the F# compiler service and its own .NET runtime. With it, compiling F# needs no .NET SDK, no .NET runtime and no dotnet command on the machine.

Releases do not publish this folder yet. Build it from a checkout, on a machine with the .NET SDK, for the runtime identifier of the target machine:

dotnet publish src/Morphir.Frontend.FSharp.Extension -c Release -r win-arm64 -p:PublishProfile=SelfContainedProcess -p:MorphirRidSpecificPublish=true -o morphir-fsharp-frontend

MorphirRidSpecificPublish=true writes the publish’s lock files under obj/, so the committed packages.lock.json files do not change. The build target PublishFSharpFrontendProcess --rid <rid> publishes the same folder, a zip (Windows) or .tar.gz archive of it with the files at the archive root, and the publish’s lock file beside the archive. Copy the folder to the target machine and name its executable with --extension:

morphir compile --language fsharp --input src/Rates.fs --package-name acme/car-rental --extension "C:\Tools\morphir-fsharp-frontend\morphir-fsharp-frontend.exe"

What this supports: compiling supported F# source files (--input) of one package to Morphir IR. The compiler references are only the .NET framework, FSharp.Core and Morphir.FSharp.Annotations; there is no NuGet restore and no reference to another package or assembly. It is not a build of an arbitrary .fsproj, and it does not compile the generated code: building an application from generated F# or C# still needs the .NET SDK.

Checked on Windows 11 ARM64 (win-arm64), on a machine with .NET installed but hidden from the frontend: the folder runs from a path with a space, with no dotnet on PATH, no DOTNET_* variables, no global install and an empty NuGet packages folder. It loads the .NET runtime from its own folder, and at the same runtime version it gives the same IR and diagnostics as the framework-dependent Morphir.Frontend.FSharp.Extension tool. It was not run on a machine with no .NET at all. The other platforms are not qualified yet; see Installation.

16.4 - Try a local managed toolchain candidate

Developer journey: install, select and run a locally built morphirup toolchain candidate

This Windows x64 development journey installs an explicitly supplied candidate through the real standalone morphirup manager. It uses the whole untrimmed self-contained managed Morphir folder, including its runtime, F# compiler service and reference assemblies. A single executable copied from that folder will not work. No system .NET SDK is needed by the installed host’s file-input workflow.

Status: the actual standalone manager installed, selected and showed the candidate, then compiled and generated the frozen outputs. The same installed selection repeated successfully after acquisition inputs were withdrawn and system .NET was hidden. Both phases matched the frozen IR and exact 112-byte F#; all 320 installed files, receipt and selection stayed unchanged. This is a local AMD64-artifact journey on Windows ARM64 emulation, not native physical x64, acquired-release, production or full acceptance qualification. Commands below follow the root-authored local candidate contract at docs/spec/toolchain/local-developer-candidate.md in finos/morphir, whose publication is being coordinated separately. Their compile/generate options were confirmed by root against the actual packaged executable; the harness also captures its help through the manager. The retained evidence below binds the successful journey to exact local bytes.

Inputs

Obtain the standalone Windows x64 manager, full candidate ZIP and its producer’s candidate.json sidecar. Keep the exact archive digest and producer source/build identity with them. local-developer is explicit consent to run those local bytes; a checksum does not authenticate the publisher. The candidate family is local-toolchains/0.1.0-preview.1, separate from authenticated release metadata.

The candidate’s raw JSON digest names its generation. Its archive.sha256 names the full host folder. Its files array binds every published file, including runtime, FCS, backend and reference assemblies. The two identities are distinct. Do not edit/reformat the sidecar after recording its digest.

The existing official producer command is:

dotnet run --project build/_build.csproj --configuration Release -- --target PackRelease --deployment self-contained --rid win-x64 --release-dir .dev/local-mvp/host-release

This is a producer command requiring the pinned SDK, not a command the consumer needs to install the candidate. It selects Deployment.SelfContained in build/Helpers/ReleaseProfiles.cs, publishes src/Morphir.Managed, checks the folder through the existing release layout check, and archives every file through the existing release packer. It sets MorphirDeployment=self-contained, SelfContained=true, PublishTrimmed=false and PublishSingleFile=false. The legacy single-file and Native AOT targets are different offerings.

Root produced the current development ZIP with the existing 0.4.0-alpha.4 identity at source eb7d164bccbb24e069087a61c0dd1ffffff326e5. The retained build-contents sidecar discloses a dirty working tree. The ZIP is 58,042,046 bytes, SHA-256 bff0dbad0d70b15843237ab74f163c014e858ccc6e8aad8a7ec654176a7c1a6e, with 320 files and 136,932,607 extracted bytes. This is a source-built local candidate, not a new version choice, release or authenticated acquisition. Use the actual candidate sidecar produced alongside it, never a fabricated one. The actual host reports the full SemVer 0.4.0-alpha.4+eb7d164bccbb24e069087a61c0dd1ffffff326e5. The handed-off raw candidate sidecar has SHA-256 89ee87439452be1a0afd294fc40af0cb2aea9d3f80b0bb15d922a964134ca037. The archive release label and exact host version remain separate facts.

The supplied standalone native manager was produced by the existing PackMorphirup target. Its AMD64 executable is 6,822,400 bytes, SHA-256 393b710f37066dff35443a6817034830f5d5cd190ac789310dd18fbd1f963aae. The supplied bundle retains this identity in EVIDENCE.json. A source checkpoint rebuilt later has its own exact native digest and producer commit in the milestone PR and bundle evidence; never relabel a rebuild as the same bytes. These are local artifacts; they are not a published production release or complete acceptance evidence.

Produce another explicit local candidate

Producer builds require the pinned SDK and the Windows native compiler/toolchain; consumer commands below do not. Use fresh output directories. These commands produce local bytes with their own full version, raw manifest and archive digests; they do not reproduce an older candidate identity or publish a release.

$ReleaseDir = Join-Path (Get-Location) 'artifacts/local-managed-candidate'
dotnet run --project build/_build.csproj --configuration Release -- --target PackRelease --deployment self-contained --rid win-x64 --release-dir $ReleaseDir
if ($LASTEXITCODE -ne 0) { throw 'Host packing failed' }
dotnet run --project build/_build.csproj --configuration Release -- --target PackMorphirup --rid win-x64 --release-assets-dir artifacts/local-manager-candidate
if ($LASTEXITCODE -ne 0) { throw 'Native manager packing failed' }

Author the explicit local sidecar from that exact archive and its build origin. This writes declarations and hashes, not an independent installer or verifier. Setup still validates the archive, complete file closure, executable and actual installed version before it writes a launch-ready receipt.

$Archives = @(Get-ChildItem -LiteralPath $ReleaseDir -Filter '*-self-contained-win-x64.zip' -File)
if ($Archives.Count -ne 1) { throw 'Use a fresh directory with exactly one host archive' }
$Archive = $Archives[0].FullName
$Contents = Get-Content -LiteralPath ($Archive + '.build-contents.json') -Raw | ConvertFrom-Json
$ArchiveSha = (Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash.ToLowerInvariant()
if ($ArchiveSha -ne $Contents.sha256) { throw 'Archive differs from its build origin sidecar' }
$PublishedHost = Join-Path $ReleaseDir 'publish/self-contained/win-x64/morphir.exe'
$Version = (& $PublishedHost --version | Out-String).Trim()
if ($LASTEXITCODE -ne 0) { throw 'Published host version failed' }
$Zip = [IO.Compression.ZipFile]::OpenRead($Archive)
try {
    $Files = @($Zip.Entries | Where-Object { -not $_.FullName.EndsWith('/') } | Sort-Object FullName | ForEach-Object {
        $Stream = $_.Open()
        try { $Hash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($Stream)).ToLowerInvariant() }
        finally { $Stream.Dispose() }
        [ordered]@{ path = $_.FullName; bytes = $_.Length; sha256 = $Hash }
    })
} finally { $Zip.Dispose() }
$Entry = @($Files | Where-Object { $_.path -ceq 'morphir.exe' })
if ($Entry.Count -ne 1) { throw 'The whole-folder archive needs exactly one morphir.exe entry point' }
$Document = [ordered]@{
    '$schema' = 'https://morphir.finos.org/schemas/local-toolchains/0.1.0-preview.1/candidate.schema.json'
    origin = 'local-developer'; implementation = 'dotnet'; deployment = 'self-contained'; platform = 'win-x64'
    version = $Version; sourceCommit = $Contents.origin.commit
    buildIdentity = ('local official SelfContained build; workingTreeClean=' + $Contents.origin.workingTreeClean)
    sourceRepository = 'https://github.com/finos/morphir-dotnet'
    archive = [ordered]@{ format = 'zip'; sha256 = $ArchiveSha; bytes = (Get-Item -LiteralPath $Archive).Length }
    entryPoint = [ordered]@{ path = 'morphir.exe'; sha256 = $Entry[0].sha256 }
    files = $Files
}
$Candidate = Join-Path $ReleaseDir 'candidate.json'
if (Test-Path -LiteralPath $Candidate) { throw 'Preserve existing candidate bytes; choose a fresh output directory' }
[IO.File]::WriteAllText($Candidate, ($Document | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false))
Get-FileHash -LiteralPath $Candidate -Algorithm SHA256

Keep both build sidecars with the archive and disclose the recorded dirty/clean producer state. Selection uses the new raw JSON digest. Do not edit the frozen manifest used by the retained milestone evidence.

Install and select explicitly

Set these paths to the extracted candidate bundle and an empty absolute Home. The consumer needs neither this repository nor a system .NET installation.

$Bundle = '<absolute path to the extracted candidate bundle>'
$Manager = Join-Path $Bundle 'morphirup.exe'
$Candidate = Join-Path $Bundle 'candidate.json'
$Archive = Join-Path $Bundle 'morphir-dotnet-0.4.0-alpha.4-self-contained-win-x64.zip'
$MorphirHome = Join-Path $env:TEMP ('morphir-local-' + [Guid]::NewGuid().ToString('N'))
$CandidateSha = (Get-FileHash -LiteralPath $Candidate -Algorithm SHA256).Hash.ToLowerInvariant()
$Declared = Get-Content -Raw -LiteralPath $Candidate | ConvertFrom-Json
if ((Get-FileHash -LiteralPath $Archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $Declared.archive.sha256) { throw 'Archive digest differs' }

& $Manager setup --candidate $Candidate --local-archive $Archive --home $MorphirHome --local-developer --dry-run
& $Manager setup --candidate $Candidate --local-archive $Archive --home $MorphirHome --local-developer --yes
& $Manager select --generation $CandidateSha --home $MorphirHome
& $Manager show --explain --json --home $MorphirHome
& $Manager run --home $MorphirHome -- --version

Check each command’s exit code before continuing. Dry-run must leave Home absent and execute no candidate code. Setup installs without selecting a new default. Select writes the Home-local pointer explicitly. Setup/select/show report origin: local-developer, the raw candidate identity and the Home-relative receipt path. Run forwards the selected host’s output and exit status.

Installed bytes live in store/tools/sha256/<archiveSha256>/. The exact candidate and generated receipt live in toolchains/generations/<candidateSha256>/. toolchains/default.json binds that receipt’s path and digest. These are the root contract’s paths; the extension-specific single-file store is not used here.

Compile the frozen source and generate F#

Capture the actual command help first. The executable, rather than an assumed developer entry point, must accept the following options.

& $Manager run --home $MorphirHome -- compile --help
& $Manager run --home $MorphirHome -- generate --help
$Fixture = Join-Path $Bundle 'sample'
$Project = Join-Path $env:TEMP ('morphir-pricing-' + [Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $Project | Out-Null
Copy-Item -LiteralPath (Join-Path $Fixture 'Pricing.fs') -Destination (Join-Path $Project 'Pricing.fs')
$Compiled = Join-Path $Project 'compiled'
$Ir = Join-Path $Compiled 'morphir-ir.json'
$Generated = Join-Path $Project 'generated'
Push-Location -LiteralPath $Project
try {
    & $Manager run --home $MorphirHome -- compile --language fsharp --input Pricing.fs --package-name acme/demo --ir-version 4 --output compiled --out-dir out --json
    if ($LASTEXITCODE -ne 0) { throw 'Compile failed' }
    & $Manager run --home $MorphirHome -- generate --target fsharp --input compiled/morphir-ir.json --output generated --option format=false --json
    if ($LASTEXITCODE -ne 0) { throw 'Generate failed' }
} finally { Pop-Location }
Get-FileHash -LiteralPath (Join-Path $Generated 'Pricing.fs') -Algorithm SHA256

An earlier manager refused generate after compile created legitimate Home/locks/install/ state. The current manager keeps strict setup admission and validates its owned state for run/show/select, allowing the host’s separate state to coexist. The successful test kept the same Home throughout; it did not delete locks, redirect the host’s Home or bypass identity checks.

The authority is the existing fixed workflow case.json, not a newly generated expectation. Its IR comparison ignores JSON object member order. Its generated Pricing.fs must match the frozen file byte for byte: exactly 112 bytes, SHA-256 64e08313f3e57be2c18995a2e8b275db9d921341958da2c8cd960c601c066de0. Keep formatting off. A different result is a failing journey, not permission to update the fixed expectation.

Actual-executable regression and offline evidence

LocalDeveloperToolchainTests.InstallsSelectsCompilesAndGeneratesOfflineWithoutSystemDotnet in the existing E2E project consumes only these explicit inputs:

$env:MORPHIR_LOCAL_MANAGER_EXE = $Manager
$env:MORPHIR_LOCAL_CANDIDATE_MANIFEST = $Candidate
$env:MORPHIR_LOCAL_CANDIDATE_ARCHIVE = $Archive
$env:MORPHIR_LOCAL_CANDIDATE_SHA256 = $Declared.archive.sha256
$env:MORPHIR_LOCAL_E2E_REPORT = '<new empty absolute evidence directory>'

The harness has fixed manager command vectors and fixed output expectations. There is no command/assertion plugin or fake manager. It uses the existing TestSandbox, ChildProcess, packaged-artifact metadata and runtime-trace helpers. It verifies the supplied archive against the explicit digest and existing build-launch sidecar, starts with an empty isolated Home and copies the fixed source into a temporary project. It captures help from the real selected host, compiles and generates through the manager, and checks the frozen outputs.

For the repeat, it withdraws its own acquisition-input copies, retains the same installed selection, and removes runtime environment/PATH access through the existing hiding plan. A native Windows where.exe copied as dotnet.exe becomes the hostile PATH/DOTNET_HOST_PATH guard; an explicit --version control must fail. No new guard binary or framework-dependent Morphir surrogate is built. The configured test refuses to claim runtime hiding if Windows has a registered .NET location that the existing hiding mechanism cannot hide. It never changes the machine registry or system installation.

Both phases require host traces showing that the selected runtime loads only from the installed folder. The harness independently hashes the entire installed files closure at installation, before the repeat and afterward, refusing extra, missing or linked entries. The receipt and selection bytes must stay unchanged. Blocked proxy endpoints and withdrawn inputs make acquisition unavailable to the local command path; these controls do not claim an OS-level network sandbox.

Raw stdout/stderr, exact argument vectors, UTC child timings, runtime traces, input sidecar, build provenance, receipt/selection bytes, whole-folder hash observations and both fixed outputs remain in the supplied evidence directory. Successful evidence records actual manager/host/runtime digests and OS/test process architecture, so x64 execution under emulation is not confused with a native x64 machine. Children are hidden, bounded, killed as a tree on timeout and awaited through TestSupport. Temporary ownership uses awaited sandbox cleanup.

The successful run used one fresh no-restore build, zero warnings/errors, and one exact filtered test: one passed, zero failed/skipped, 41.745 seconds. Raw build and runner streams, UTC metadata, source/runtime closure and release inventory are under .dev/local-mvp/e2e-actual-batch4/. Actual command/phase evidence is in .dev/local-mvp/actual-native-journey-attempt5/, including both IR/F# outputs, receipt/pointer bytes, three full-folder hash observations, raw stdout/stderr and app-local runtime traces. The hostile native dotnet --version control exited 1; offline show, compile and generate each exited 0.

Earlier D4D2 and FF118 successful functional journeys remain immutable in e2e-actual-batch2/, e2e-actual-batch3/ and their phase directories. The current 393B artifact follows the launch ownership and cleanup corrections. Its normal journey is verified here. The separate independent ownership/interop recheck cleared the process-tree and final-disposal corrections. Nine launch controls passed in Release and Debug with the active-configuration fixture copies verified. Those are separate evidence, not conclusions of this functional test.

Two earlier failures remain immutable under .dev/local-mvp/e2e-actual-batch/ and actual-native-journey{,-attempt2}/. One exposed a harness assumption about help’s output stream, corrected to inspect captured help across both streams. The other exposed the Home-state integration refusal fixed in the new manager. That earlier compile’s IR was compared but not retained before cleanup. The successful source now copies phase outputs before later checks, preserving all existing assertions and fixed bytes. Historical failures are not relabeled as passes, and root’s separate direct-host proof is not substituted for this run. Without explicit inputs the opt-in test is ignored, not counted as a passed milestone. Full installer breadth, orphan sweep, independent process providers, portable-lock restore, other offerings/platforms, production authentication and final acceptance remain separate unfinished work.

macOS ARM64 local candidate

On a Mac with Apple silicon, the build makes the macOS ARM64 inputs:

./build.sh --target PackWindowsToolchain --rid osx-arm64 --deployment self-contained \
  --toolchain-local-developer --release-assets-dir artifacts/osx-toolchain

The target keeps its name for the workflows that call it. For osx-arm64 it writes the host archive morphir-dotnet-<version>-self-contained-osx-arm64.tar.gz, the candidate morphir-host-candidate-osx-arm64.json, the F# frontend and backend archives, release descriptors and manifests, morphir-extensions-osx-arm64.json, the bundle morphir-toolchain-osx-arm64.json, SHA256SUMS and the manager morphirup-osx-arm64. The archives are tar.gz, and the entry points are morphir, morphir-fsharp-frontend and morphir-fsharp-backend, with no .exe. Then, with A=artifacts/osx-toolchain and an absolute Home H:

$A/morphirup-osx-arm64 setup --candidate $A/morphir-host-candidate-osx-arm64.json \
  --local-archive $A/morphir-dotnet-<version>-self-contained-osx-arm64.tar.gz \
  --local-bundle $A/morphir-toolchain-osx-arm64.json --local-developer --home "$H" --dry-run
# The same command with --yes instead of --dry-run installs and prints generationSha256.
$A/morphirup-osx-arm64 select --generation <generationSha256> --home "$H"
cd examples/morphirup-fsharp
$A/morphirup-osx-arm64 run --home "$H" -- compile --output compiled --out-dir out --json
$A/morphirup-osx-arm64 run --home "$H" -- generate --target fsharp --input compiled/morphir-ir.json \
  --output generated --option format=false --json

The installed commands need no .NET on PATH and no network. What setup and run do differently on macOS is in the morphirup reference. This is local-developer delivery: a checksum does not authenticate the publisher.

17 -

Inspecting a resolved configuration

Morphir.Configuration exposes pure Inspection.get, list, show, path and explain functions. They take a QueryScope and ResolutionDetails. get and explain also take a segmented key path. No operation reads files, consults the environment, constructs the CLI, executes scripts or resolves secret references.

Use Resolver.resolveDetailed(input) for supplied sources, or ProjectSystem.LoadDetailed(tree, discovery, project, options) from Morphir.Configuration.IO. Both resolve once and retain loaded canonical layers alongside their ownership-filtered contributions. Their Resolution property is the existing DocumentResolution; Resolver.resolve and ProjectSystem.Load keep their original signatures and results.

open Morphir.Configuration

let details = Resolver.resolveDetailed suppliedInput
let key = KeyPath.parse "codegen.\"custom.generator\".enabled"
match key with
| Ok segments -> Inspection.get QueryScope.Effective segments details
| Error message -> failwith message // map to a usage diagnostic at the host edge
using Microsoft.FSharp.Collections;
using Morphir.Configuration;
using Morphir.Configuration.IO;

var tree = new InMemoryProjectTree("/repo", new Dictionary<string, string>
{
    ["/repo/morphir.toml"] = "[project]\nname='Example'\n[custom]\nitems=[]\n"
});
var discovery = ProjectSystem.Discover(tree, "/repo");
var details = ProjectSystem.LoadDetailed(tree, discovery.Discovery);
var key = ListModule.OfSeq(new[] { "custom", "items" });
var item = Inspection.get(QueryScope.Project, key, details);
// Found is true; Value contains ConfigValue.Array with no items.
var history = Inspection.explain(QueryScope.Effective, key, details);
var sources = Inspection.path(QueryScope.Effective, details);

Key grammar

Paths are ordinal and case-sensitive. Plain segments contain ASCII letters, digits, _ and -; dots separate segments. Double-quoted segments are JSON strings, so codegen."custom.generator".enabled addresses one literal dot in a name. Quotes, backslashes, control characters, colons, non-ASCII characters and empty names use quoted segments. KeyPath.format produces this grammar and KeyPath.parse returns a typed Result. Leading/trailing dots, empty plain segments, whitespace outside quotes and bracket indexing are invalid. Arrays and secret references are whole values, not traversable paths.

Scopes and results

QueryScope.Effective uses the resolved document. Workspace, Project, User, Global, System and File(path) select source groups before query merging. A scope never fills a missing key from defaults, environment or another scope. Workspace/project mean primary files, not their profile/personal layers. User means the override adjacent to the inspected target. File identities are canonical absolute paths supplied by the host. Scoped inspection reads each source’s own normalized values; effective queries retain ownership filters.

File scope excludes synthetic compatibility defaults even when they share the manifest’s path. For a classic manifest with no authored pipeline.backend, the file-scoped query reports missing while the effective query can still return the compatibility value checkpoint.

InspectionResult has Operation, Scope, optional Key, Found, optional Value, Entries, Sources, Contributions, and Diagnostics.

  • get returns one typed value and an entry with its origins. Found=false differs from a present empty string, array, object, or null array element.
  • list returns ordered leaf entries; arrays, empty objects and secret references are indivisible leaves. Each entry carries a formatted Key, segmented Path, typed Value and ordered Origins.
  • show returns the nested document plus entries and origins.
  • path returns consulted source identities, load statuses, contribution flags and precedence order. Missing optional, skipped and failed sources stay visible.
  • explain returns relevant ordered layer contributions and marks surviving origins as winners. A composite object can retain winners from several layers. An ancestor shape replacement can block a formerly present descendant.

Check Diagnostics for errors before consuming a result. CLI exit codes are 0 for success, 1 for a missing queried key, and 2 for usage/loading errors. The CLI reference documents the generated JSON report, including source and diagnostic schemas.

Redaction

Every inspection operation calls the same recursive presentation redactor. Secret references become the string [REDACTED]. A path containing a sensitive name also becomes [REDACTED], including descendants in objects and arrays. Matching ignores punctuation and case and recognizes password, passphrase, secret, token, credential, private key, API key, access key, connection string and authorization names. This deliberately favors redaction of ambiguous names.

Diagnostic messages can contain parser input, so inspection replaces their free text with safe text and retains codes, severity, key/source identity and positions. Key names and source paths are metadata and remain visible. Hosts must keep secrets out of those identities. ResolutionDetails.Resolution and its layers remain the original canonical data for trusted application consumers; inspection redaction does not mutate or sanitize that canonical storage.

See Microsoft configuration integration for the optional standard .NET view and its explicit/source-generated binding path. Existing IR and cache commands retain their earlier configuration behavior.

18 -

Profile planning for library hosts

The additive document API plans profiles from supplied documents and controls. Profiles.plan never discovers files, reads the process environment, executes project code, or resolves secrets. Pass the environment map explicitly.

Call the planner before reading profile candidates. Read only candidates whose SourceIdentity.Profile appears in the successful plan’s Active list. Keep inactive candidates unread, including invalid files. ProfileSelection.Disabled returns an empty disabled plan without interpreting profile controls.

Selecting profiles

The selector has this order of authority:

  1. ProfileSelection.Names supplied by the host.
  2. A present MORPHIR_PROFILES environment entry, including an empty value.
  3. The discovered root primary documents’ active selector.
  4. The discovered root primary documents’ default selector.

There is no implicit CI or default profile. A comma-separated environment selector trims names. A present whitespace-only environment selector disables profiles and suppresses includes and file loading. An explicit Names [] selector still permits base includes. CSV trimming uses ASCII space, tab, newline, carriage return, vertical tab, and form feed. Empty CSV elements fail validation. Values such as dev,,metrics or dev, do not silently discard an empty name. The legacy CiProfileMode API retains its separate behavior.

Control ownership and ordering

Only discovered root primary documents own activation controls. A root primary that is also explicitly selected keeps its original control authority, while its ordinary values contribute once at the selected-file layer. Machine, member, arbitrary selected, user, and profile documents cannot activate profiles. The planner reports invalid controls before the resolver filters ownership subtrees.

profiles.include expands before selected roots. Groups emit their own name before expanding members depth first, and duplicate names keep their last occurrence. A higher group definition replaces the whole lower definition. Enabled processing checks the entire declared group graph, including inactive groups, for cycles and depth/expansion bounds. Names match [a-z][a-z0-9_-]{0,63}. A graph contains at most 64 groups, each with at most 64 members and at most 32 group levels. The active plan contains at most 128 distinct names. Host options may tighten depth and expansion bounds.

For example, profiles.groups.production = ["postgres", "metrics"] and profiles.active = ["production"] activate production, postgres, then metrics. A higher profiles.active = null fails control typing even if a lower source supplied a valid list. Ordinary null overlay skipping does not apply to profile controls.

Resolution applies each active name’s workspace documents before its member documents, using JSON/JSON5, then YAML/YML, then TOML. All files for the next active name follow all files for the previous name. Adjacent user overrides, environment values, and explicit host values follow profiles.

Diagnostics and integration

Inspect ProfilePlan.Diagnostics before reading any planned profile files. Errors identify the declaring control’s source and key path. The resolver retains winning origins separately from the control plan. An explicit host owns the source inventory and workspace/member identity. The library does not infer either from current directories or ambient environment state.

The qualification baseline is Moon-Lander d6216c7469c045548cb15b3f9321857ac41d2cd0, specifically pkgs/config/profile_types.mbt, pkgs/config/profiles/profiles.mbt, and pkgs/config/profiles/control_layers.mbt. Its control merge keeps nulls until validation, unlike ordinary configuration merging.

19 -

Resolving explicit configuration sources

Morphir.Configuration provides additive immutable document contracts and pure resolution. Its only NuGet dependency is FSharp.Core. The existing MorphirConfig, ConfigLayer, ConfigResolution, Merge functions and CiProfileMode remain available. Existing tooling continues its Git-first, cache-only TOML resolver and CI overlay until a caller explicitly adopts the document API.

Morphir.Configuration.IO reads supplied text and explicit file paths without constructing the CLI. Its explicit reader API does not search for a root; ProjectSystem provides opt-in bounded discovery. Use ConfigurationReader.ReadText, ReadFile, or ConfigurationLoader for an explicit candidate inventory. See its reader profile and the profile guide.

Host responsibilities

The host supplies source identities, canonical path identities, source roles, format groups, required/optional status, an environment snapshot and explicit values. Use the same canonical SourceIdentity.Path for a selected discovered file and its root/member inventory entry. The core compares these strings ordinally; filesystem identity, symlinks and platform path normalization belong to the host. Order orders supplied sources within the same role/format. Paths break ties deterministically.

The host also supplies ProjectScope and a separate OwningWorkspace context. Resolution never infers ownership, reads ambient process state, executes project code, resolves secrets, or mutates input trees. A non-CLI host can construct typed trees and call Normalization.normalize, then pass those documents directly to Resolver.resolve. The same normalized documents yield the same values, origins and diagnostics as the reader adapter.

open Morphir.Configuration

let result =
    Resolver.resolve
        { DocumentDefaults.resolutionInput with
            Sources = suppliedDocuments
            Environment = environmentSnapshot
            Explicit = explicitTypedValues }

let name = ConfigTree.tryFind ["project"; "name"] result.Effective
let origin = Map.tryFind ["project"; "name"] result.Origins

Check error diagnostics before consuming an effective document. Failed inputs produce structured errors and source reports, rather than exceptions for normal configuration failures. An explicitly selected missing file is always an error; optional misses remain visible in Sources.

Source and value order

The ascending source ladder is defaults, system, global, root primaries, selected file, member primaries, active profiles, adjacent user overrides, environment, and explicit values. Ordinary file groups merge JSON/JSON5, YAML/YML, then TOML. Each active profile name contributes workspace formats then member formats, before the next name. A selected discovered file contributes ordinary settings only once, at the selected layer, while retaining its base control authority.

The core supplies the pinned baseline defaults: project source src, workspace output .morphir/out, Scheme frontend, and IR v4 single-file JSON. Host defaults can override these at the defaults layer. Classic checkpoint defaults are a separate low-priority contribution, so system/global settings override them.

Objects recurse, arrays replace completely, and incompatible shapes replace. Secret references are indivisible typed leaves. Null overlays leave lower values and origins unchanged. Completed ordinary loads omit null object fields, including objects inside arrays, while retaining null array elements. Apply sources with an ordered left fold; regrouping shape changes is unsafe.

Winning leaf, empty-object and whole-array origins retain the source and any available source position. Shape replacements remove stale child origins. Consulted-source reports include loaded, skipped, missing and failed inputs. Profile controls are excluded from ordinary values and origins.

Ownership filters

System/global documents cannot supply any part of project or workspace. For member resolution, inherited root primaries, workspace profiles, selected root primaries, and root-adjacent user overrides exclude those entire subtrees. Future project/workspace fields therefore cannot leak across ownership scopes. The member keeps its project identity and defaults while inheriting applicable tooling settings.

Use SourceRole.WorkspaceUserOverride for a root-adjacent override inherited by a member, and UserOverride for an override adjacent to the target project. Member workspace.out_dir and workspace.output_dir are filtered with config.member_output diagnostics. They cannot relocate the supplied owning workspace output context. Profile files also cannot redefine workspace root, members, exclusions or default-member topology.

Profile-control authority is checked before ownership filters. A forbidden profiles subtree cannot hide inside a source whose ordinary values are filtered.

Environment and diagnostics

MORPHIR_ uses __ for nesting, keeps single underscores, and lowercases key segments. Booleans, integers and JSON arrays/objects retain their types. Integers beyond Int64 use ConfigValue.BigInteger. Other values remain strings. A conflicting shorter path wins regardless of environment enumeration order. Origins identify the winning variable. The reserved controls are MORPHIR_HOME, MORPHIR_PROFILES, MORPHIR_DIAGNOSTICS__SUPPRESS, MORPHIR_CONFIG, MORPHIR_CONFIG_FILE, MORPHIR_PROJECT, MORPHIR_WORKSPACE, and the containment controls MORPHIR_DISCOVERY_BOUNDARY and MORPHIR_ISOLATED, which hosts read and never map into configuration.

Validation uses the pinned canonical schema, including nested task/workflow and toolchain shapes. Unknown permitted properties remain in the typed document. The generated pure rules can be refreshed with /usr/bin/python3 scripts/configuration/generate-schema.py after reviewing and pinning a new schema. No schema file is read at runtime.

config.yaml_beside_toml identifies sibling YAML/TOML primary sources. config.json_keys_below_toml identifies JSON keys that survive their ordinary format group alongside TOML. Suppression is accepted only from explicit host options, comma-separated MORPHIR_DIAGNOSTICS__SUPPRESS, or system/global diagnostics.suppress string arrays. Workspace/member suppression cannot hide these warnings. Unknown suppression codes produce config.unknown_suppression.

Shared inspection and .NET consumers

Use detailed resolution and inspection for typed effective/scoped queries, source reports and redacted explanations without additional reads. The Microsoft adapter projects one resolved result while retaining canonical types and metadata. Neither integration opts existing IR/cache commands into every setting automatically.

20 - C# generation

Morphir.CodeGeneration.CSharp landed in PR #478, tracked by issue #474. The core accepts typed VersionedModel values in memory, preserves their source family/version, and returns immutable sources and diagnostics. The entry point is CSharpGenerator.Generate(model, options, cancellationToken). GenerationOptions.Default selects numeric C# 14, independently of the SDK. Numeric C# 15 uses the same public representation and runtime contract.

Both typed paths now lower transparent aliases, generic closed records, tuples, function and unit payloads, and closed custom cases. Generated payload properties are get-only. Both typed paths now emit literals, variables and references, application, returned functions, lambdas, nonrecursive value/function lets, conditionals, top-level self recursion, constructors, tuples, lists and closed record operations. Ordered patterns include wildcard, alias, literal, tuple, constructor, empty-list, head/tail and unit patterns. The SDK binding table covers the frozen 67-operation inventory. Generated calls execute the 207 required frozen observations in both typed families. The eight representation exclusions remain exclusions. The main specification and archived 13/13 tasks record the delivered bounded core. Archive follow-up landing and coordinator cleanup remain pending. Decoding, configuration, source publication and compiler execution belong to callers. CLI registration and a production Roslyn adapter are separate initiatives.

Host compatibility

EnvironmentEvidence
Windows arm64, .NET 10.0.14, SDK 10.0.403 Roslyn driverFresh package consumer restores, AddSource compiles, constant executes, caller cancellation returns no sources
Windows arm64, .NET 10.0.14, SDK 10.0.403 fresh in-memory consumerBoth typed families and numeric profiles generate nonempty type/function/pattern/record/SDK text; deterministic sources, cancellation and refusal checks pass
Self-contained win-arm64, net10.0, TrimMode=linkPublished consumer executes with identical observations/source hashes to its untrimmed build; no IL warnings in this smoke
Legacy .NET Framework / netstandard IDE compiler hostsUntested; Models and core require net10.0
Other IDE/runtime/OS combinationsUntested
Native AOTUntested

The demonstrated production package closure is Models, FSharp.Core 11.0.100 and FSharp.SystemTextJson 1.4.36. The test runner additionally brings Expecto and Mono.Cecil. Roslyn references come from the selected SDK only in the test host. Tooling, Ion, Pipeline and the CLI are absent from the core package closure. Language syntax compatibility does not establish IDE-host compatibility.

Numeric language profiles and compiler release channels are separate. The same shared type/function/pattern/record/SDK corpus has compiled and executed under C# 14.0 with stable SDK 10.0.403, compiler 5.9.0-1.26523.112, and C# 15.0 with SDK 11.0.100-rc.2.26504.105, compiler 5.12.0-1.26504.105. Both consumers target net10.0 and run on 10.0.14. The RC compiler is isolated under an owned test directory. Root global.json stays on the stable SDK. Both independently constructed classic/v4 models produce identical source bytes. Original classic car-rental versions 1/2/3, a labeled synthesized v4 car input, direct daily-rate inputs and a compatible v4 ecosystem type slice also compile and execute under both pinned compilers.

tests/Morphir.CodeGeneration.CSharp.Tests/verify-host.ps1 packs the real core and Models into a local proof feed and restores a fresh consumer with a private cache. The first attempt exposed the F# SDK’s implicit FSharp.Core 10.1.403 reference downgrading the required 11.0.100 assembly, causing runtime load failure. The corrected F# test host disables that implicit reference and uses the actual transitive closure. No model library was retargeted. Both attempts are retained in the owned evidence inventory.

verify-trimming.ps1 packs Models and the core into a fresh local feed, restores a separate private cache, and runs a typed in-memory consumer before and after self-contained trimming. That consumer’s package closure contains only Models, the core, FSharp.Core and FSharp.SystemTextJson. It loads no Roslyn, Expecto, Mono.Cecil or SDK runtime. The generated text still declares the SDK runtime requirement. This smoke establishes the listed consumer behavior on the tested host; it does not establish arbitrary IDE integration or Native AOT support.

Calling the core

Supply an already decoded Morphir.IR.Model.VersionedModel. Classic Library versions 1, 2, 3, 3.0.0 and 3.1.0, and v4 Library versions 4 and 4.0.0, retain their exact source version and typed family. Empty libraries succeed with an empty source list. Specs, Application and unsupported versions produce located failures with no sources. No serialization bridge or implicit migration runs.

open Morphir.CodeGeneration.CSharp

let result = CSharpGenerator.Generate(model, { LanguageVersion = 14 }, cancellationToken)
match result.Status with
| GenerationStatus.Success -> consumeSources result.Sources
| GenerationStatus.Failure -> reportDiagnostics result.Diagnostics
| GenerationStatus.Cancelled -> reportCancellation ()

The caller supplies model, the token and the three handling functions. Sources contain portable relative .g.cs paths and deterministic LF text. Encode text as UTF-8 without a BOM when publishing it. Decoding, package resolution, compilation, configuration and file publication remain caller responsibilities. Numeric 15 uses the same API and case representation. See the diagnostic contract.

Business fixture evidence

The shared parameterized test matrix executes the original classic car-rental 1/2/3 files unchanged and preserves their raw source hashes and version spelling. Independent observations require daily rate 5000, Available/Reserved/Rented/ Available lifecycle states, invoice 15000, seven typed rejections and persistence of both the original inventory and prior billing obligation.

The equivalent v4 car library is labeled SYNTHESIZED TEST INPUT. Its typed builder exists only in tests and supplies no expected result. It is neither an upstream v4 fixture nor production migration support. The existing direct v4 daily-rate fixture executes independently and returns 5000.

Whole original ecosystem examples retain their source limitations. The regulatory example references missing types; books-and-records has open-deal and close-deal draft IncompleteBody declarations. Both originals fail with located diagnostics and no partial sources. The separately labeled COMPATIBLE TYPE SLICE retains every original books-and-records type, visibility and payload, excluding exactly those two draft values. Its nonempty generated records/cases execute payload, equality/hash, nested dictionary persistence and get-only property checks. This is selected-input evidence, not full-original conformance.

Type mappings and equality

Morphir typeC# representation
Basics Int, Float, Boolint, double, bool
String, Char, Decimalstring, char, exact admitted decimal
Unit, OrderMorphir.SDK.CSharp.Unit, Order
List, Maybe, ResultSDK immutable wrappers, Result<E,V> with error first
Dict, SetSDK immutable wrappers with Morphir comparers
Tuple with at least two itemsC# value tuple
FunctionNested System.Func<Input,Output>
Closed recordSealed class with constructor and get-only payloads
Custom typeClosed abstract base with nested sealed case classes

Transparent aliases expand before emission. Structurally equivalent records use one deterministic representation, including aliases with substituted generic arguments. Unused alias parameters do not change the underlying representation. Anonymous record payloads receive deterministic named record classes. Private constructors remain inaccessible from a separate assembly. A public payload exposing a private nominal type fails with private-type-exposure.

Every generated record and case implements IMorphirValue. Equality checks the exact generated type or case and calls Basics.Equal for every payload. Hashing uses MorphirValue.Hash for the same payloads. Typed, boxed and operator equality route through that protocol. Consumers verify nested collections, NaN, signed zero and decimal scale against the SDK contract. No CLR IEquatable fallback or reflection establishes Morphir equality. The protocol does not establish the physical immutability of arbitrary caller-supplied generic payloads.

Names, literals and functions

Package paths become escaped namespaces. A module named Example.Functions uses the static class Example.FunctionsModule; records and cases are nested under that class. Source paths use safe prefixes, such as p_Example/m_FunctionsModule.g.cs, including on Windows. Portable comparisons ignore case. A collision diagnostic identifies both owners. Generated local identities are fresh within each function and do not depend on source spelling. Names that collide with a required structural protocol, generic parameter or inherited CLR member fail with a located name-collision. Malformed typed names fail with invalid-name. An empty library succeeds without allocating a package namespace or file path.

Top-level definitions emit typed static methods. Function values and partial applications use nested Func values that call those methods. Local bindings evaluate before their name enters scope, and local functions capture their original outer bindings. Delegate invocation uses .Invoke, and arguments evaluate once in source order. The shared compiled consumer observes those properties through a caller-supplied callback, generic identity, a captured let, shadowing, a three-argument partial application and a returned lambda.

Literals use culture-independent numeric text and explicit UTF-16 escapes. Supplementary strings are admitted. Char requires one non-surrogate BMP scalar. Int literals must fit checked Int32. A v4 decimal lexeme must hold exactly in Decimal96; rounded or out-of-range values fail with representation-loss. Signed-zero Float bits are preserved, and classic nonfinite Float literals map to the corresponding double constants. Invalid UTF-16 strings fail explicitly.

Type checking runs before a successful source set is returned. Unknown variables and references, inconsistent applications, infinite types and unsupported numeric constraints produce located diagnostics. Local recursive groups remain outside the admitted slice. The business fixture scope and evidence are described above.

Cancellation checks cover typed traversal, recursive alias/record expansion, name allocation, SDK comparer construction and text emission. The final result boundary checks the token again before returning success. Cancellation returns Cancelled, the original typed family/source version, and no sources. The active-generation tests interrupt alias expansion and million-character strings in both families. Their measured stop times describe that bounded test run, not a general latency guarantee. Invalid later modules also return no partial source set.

Matches evaluate their subject once and select the first matching branch. Destructuring and pattern lambdas bind fresh lexical identities after their pattern succeeds. An unmatched non-exhaustive match or destructuring raises a pattern failure. This is the source operation’s failure, with all branches checked during generation. Unsupported source operations fail generation.

Record expressions evaluate fields in source order. Updates evaluate the target once, then update values in source order, and construct a new immutable record. Constructor argument order follows the selected structural record declaration. The classic model’s record map has key order; v4 retains its authored field order. Field functions require a known closed record type. Missing fields, incompatible updates, duplicate binders and inconsistent branch types produce located failures.

SDK bindings

Generated code calls the public Morphir.SDK.CSharp API. The generator package does not load that runtime to generate text. Its declared output requirement is net10.0 plus Morphir.SDK.CSharp, independently of the compiler host’s packages.

Source moduleAdmitted runtime calls
BasicsChecked Int32/IEEE Float arithmetic, conversion/rounding, comparisons, min/max/clamp, Order cases and structural equality
Listsingleton, cons, map, append, head, tail and length
MaybeJust/Nothing constructors and patterns, map and andThen
ResultOk/Err constructors and patterns, map and andThen; error type first, value type second
Dictempty, singleton, fromList, get, insert, update, remove, toList and union
Setempty, singleton, fromList, insert, remove, toList and union
DecimalExact add, sub, mul, div, compare and toString

Map and andThen use the actual instance combinators. Nothing and Err bypass callbacks, and callback exceptions propagate. SDK constructors also work as function values. Dictionary/set factories select MorphirComparers.Int, Float, Char or String, composing list and two/three-tuple comparers as needed. No CLR default comparer or reflection fallback is generated.

Ordering requires concrete admitted comparable shapes, with finite Float values. Bool, Decimal, Unit, Order, records, variants and function shapes have no SDK ordering. A factory whose key comparer cannot be resolved fails with unsupported-comparable. Known function payloads fail equality admission with unsupported-equality. Unmapped SDK references fail with unmapped-sdk; dependencies without an implementation fail with unresolved-reference or unresolved-type.

Type/literal incompatibilities are generation diagnostics. Value-dependent SDK failures, such as checked overflow or nonfinite Float-to-Int conversion, remain the SDK’s execution errors. Decimal operations preserve the exact Decimal96 subset and do not round unsupported fractions. Ordinary Decimal equality ignores authored scale; Decimal arithmetic produces canonical values. The generated-call corpus checks frozen values/errors, input persistence and equality/hash invariants.

20.1 - C# generation API and diagnostics

CSharpGenerator.Generate(VersionedModel, GenerationOptions, CancellationToken) is synchronous and has no file, process, configuration or compiler dependency. GenerationOptions.LanguageVersion accepts numeric 14 or 15; the default is 14. Compiler release channel is chosen by the caller independently.

The existing GenerationOptions record and three-argument entry point are unchanged. To choose an emitted namespace, call NamespaceOverride.TryCreate(string). It returns Result<NamespaceOverride, GenerationDiagnostic>; a successful immutable value exposes Value. Pass it to CSharpGenerator.Generate(model, options, namespaceOverride, cancellationToken). Both overloads use the same core implementation. An override applies to the input Library package and all its qualified type, value and constructor references. Package-derived source paths and the original typed IR remain unchanged. Separate or concurrent requests never share naming state.

The namespace accepts dotted literal BMP C# identifiers in Unicode Normalization Form C, including @-escaped reserved words. It admits the identifier character categories defined by the C# lexical specification. Formatting characters retain their literal spelling and have the compiler’s identifier-equivalence behavior. Source \u/\U escape syntax and surrogate characters are outside this literal-name boundary. Values are validated rather than normalized or interpreted as source text. Null, empty, malformed and injection-like input returns invalid-namespace at $options/namespace. An explicit null/default namespace value from a C# caller to the four-argument overload returns that same located refusal and no sources. An already cancelled caller still receives Cancelled first.

GenerationResult returns Status, Family, exact SourceVersion, selected LanguageVersion, Sources, Diagnostics and Runtime. Each source has RelativePath and Text. The runtime requirement is net10.0 plus the public Morphir.SDK.CSharp package. The core package itself does not reference that runtime. Consumers must resolve a runtime implementing the frozen SDK contract.

Success returns the complete source set and no diagnostics. Failure returns the first located refusal and no sources. Cancelled returns no sources or diagnostics, retaining input family/version and the selected profile. A caller that needs exception-based cancellation can translate this status at its edge. Cancellation checks cover traversal and emission, with a final token check before success. Invalid later modules also discard all earlier generated text.

Diagnostics carry stable Rule, structural Location and human-readable Message fields. Locations identify the version/options/distribution or the package/module/type/value/pattern/expression being checked. Callers should branch on Rule, not parse messages. The CLI successor can map these categories without loading a compiler or changing core semantics.

Diagnostic identities use explicit structural spellings, for example Name["car","rental"], ModulePath[Name["inventory"]] and FQName(PackagePath[Name["example"]],ModulePath[Name["data"]],Name["person"]). Raw words and path segments remain distinguishable. Strings are quoted, quotes and backslashes are escaped, null strings are written null, and control or surrogate characters use \u followed by four hexadecimal digits. Type mismatch messages describe Parameter(Name[...]), Named(FQName(...),[...]), Product([...]), Fields[Name[...]:...,Name[...]:...], Arrow(...,...) or Unit. These diagnostic-only spellings avoid structural reflection in NativeAOT. Module validation locations now use $modules/ModulePath[...]. This intentionally changes diagnostic text and that location spelling while retaining rule IDs, refusal and cancellation behavior, generated source bytes and paths. Callers should continue branching on Rule rather than parsing human messages.

CategoryRules
Input/profile boundaryunsupported-language, unsupported-version, unsupported-distribution, invalid-namespace
Target names and portable pathsinvalid-name, name-collision, path-collision, path-length
Type identity and closureunresolved-type, unmapped-sdk-type, unbound-type, type-arity, tuple-arity, recursive-alias, ambiguous-type, anonymous-record, private-type-exposure
Type/representation constraintstype-mismatch, open-record, unsupported-constraints, unsupported-numeric, unsupported-comparable, unsupported-equality, representation-loss, document-literal
Value and constructor resolutionunbound-variable, unresolved-reference, unresolved-constructor, ambiguous-constructor, constructor-arity, unmapped-sdk
Unsupported source semanticsunsupported-body, unsupported-expression, local-recursion
Patterns and recordsduplicate-pattern-binding, empty-match, unknown-field

Unknown dependencies and SDK operations fail explicitly. No object-erased value, runtime stub or success-shaped partial artifact substitutes for a refusal. Admitted value-dependent SDK failures, such as checked overflow, remain execution errors. An admitted non-exhaustive source pattern raises pattern failure when no branch matches; every branch is still checked during generation.

Generated records/cases have get-only payloads and explicitly implement the SDK structural equality/hash protocol. That protocol alone does not establish the physical immutability of arbitrary externally supplied CLR generic payloads. The tested Morphir inputs use the admitted typed representations and immutable SDK collections. No arbitrary CLR IEquatable or reflection fallback is emitted.

See usage, host matrix and fixture scope and the generate command. The production AdditionalFiles/config/incremental adapter remains deferred.

21 - Discover a workspace from a host library

Find the workspace and member projects from .NET code with ProjectSystem.Discover and ProjectSystem.Load

This page uses the workspace of Configure a workspace.

For a native repository, supply an absolute boundary and start path:

using Morphir.Configuration.IO;

var tree = new FileSystemProjectTree("/repo");
var result = ProjectSystem.Discover(tree, "/repo/packages/orders/src");
// Discovery.Location: /repo
// Selection.Projects: /repo/packages/orders

A mounted host can supply the same contents with InMemoryProjectTree. Both adapters use the same pure discovery and selection policy. The result retains the complete member inventory separately from the selected project. Pass "packages/orders" as the optional selector to choose that canonical member path, even if another member has the same display name.

The boundary includes /repo and excludes its parents. An input file starts discovery in its containing directory. Only the nearest declared workspace participates; discovery does not merge configuration from every ancestor. If no workspace is found, the nearest primary location is a standalone project. With no primary, the start directory remains the location. Neither .git nor an empty .morphir directory declares a workspace through this API.

Keep missing and invalid members visible in diagnostics. A nonexistent literal member produces a warning; a literal directory with no configuration or an invalid document requires attention. Excluded members are absent from the selected membership. Read source reports to distinguish an optional miss from a read failure.

Resolve that member independently through the shared loader:

var selected = result.Selection.Projects.Single();
var effective = ProjectSystem.Load(tree, result.Discovery, selected,
    new ProjectLoadOptions { Isolated = true });

Isolation retains declared primaries and supplied values while omitting machine and personal files. For active profiles, supply the desired profile selection through ProjectLoadOptions.Input. To load the root instead, omit the ProjectInfo argument. The owning workspace context carries an absolute output directory separately from the member settings; the member does not inherit the root’s project or workspace document subtrees.

Machine files require both explicit MachineLocationInput and a separately authorized MachineTree. No ambient machine access is implied by this example. A relative SelectedPath resolves from the invocation directory, while the project selector resolves from the effective workspace root.

The dist, tool and extension commands are not implemented yet (#490, #491, #492); when they are, they will use this same discovery.

22 - F# generation

Generate F# source from a Morphir library model, built in or as a MEP extension.

The F# generator (Morphir.CodeGeneration.FSharp) turns a Morphir library model into F# source files, one file for each module. To generate F# from the command line, see Generate F# from a Morphir model and the generate command.

Status

The generator has two deliveries. Both run the same generator code.

  • Built in. morphir.fsharp is the backend of the morphir and dotnet-morphir hosts. It is the default: morphir generate --target fsharp uses it when no backend extension is selected. It reads JSON and Ion models. The managed host dotnet-morphir formats the generated source with Fantomas. The Native AOT morphir cannot run Fantomas, so by default it writes the same declarations unformatted, says so on stderr, and its files differ from the extension’s in formatting. With --option format=false every host writes the same unformatted bytes; see Unformatted output.
  • MEP extension. morphir-fsharp-backend is a Morphir Extension Protocol (MEP) 0.2.0-draft.1 guest with its own extension id, org.finos.morphir.fsharp-backend. It is for hosts that do not have the built-in backend, for example a MoonBit host. See Run the generator as a MEP extension.

morphir generate runs the extension only when you select it, with --extension or [codegen.fsharp].extension; installing it changes nothing else. Every entry point (morphir, the managed morphir and dotnet-morphir) has the option:

morphir generate --input model.json --target fsharp --output gen --extension morphir-fsharp-backend

The host starts the guest, agrees the session, sends the model’s JSON IR, and checks and publishes the files that the guest returns. The report names the extension by its own id, with "backendTransport": "mep" and its version. See Backend extensions for the configuration key, how the value is read, and the failure codes.

Run the generator as a MEP extension

Install the guest as a dotnet tool. It is framework dependent, so it needs the .NET 10 runtime.

dotnet tool install --global Morphir.CodeGeneration.FSharp.Extension --prerelease

The guest speaks only MEP 0.2.0-draft.1, over stdio with Content-Length framing. A host selects it explicitly: it starts the morphir-fsharp-backend executable, initializes a session, and sends morphir.backend.generateModel with the target fsharp. The guest claims:

ClaimValue
Claims version0.1.0-draft.2
Extension id and typeorg.finos.morphir.fsharp-backend, backend
Backend targetfsharp, for IR 4.0.0 and 3.0.0
Legacy morphir.backend.generateno
Model tuplesmorphir.backend.generateModel, profile morphir-ir, codec morphir-json, envelope 0.1.0-draft.1, IR 4.0.0 and 3.0.0
Guaranteesnone
Streaming, cancellation, progressno

The extension version is the version of its own package, not of a host.

For IR 4.0.0 the model is IR v4 JSON with formatVersion 4 or "4.0.0". For IR 3.0.0 it is classic JSON with formatVersion 3. Classic IR versions 1 and 2 and Ion models have no tuple: a host converts them first, or uses the built-in backend.

The guest refuses a request with -32602, and generates nothing, when:

  • its input tuple is not a generateModel tuple that the session agreed (Model admission refused: unsupported_tuple);
  • the envelope fails admission, for example a digest that does not match (Model admission refused: digest_mismatch);
  • it requires a guarantee (Model admission refused: unsupported_guarantee);
  • the target is not fsharp;
  • an option is not the Boolean optimize or format, which the built-in backend also accepts and which are true when absent;
  • the payload is not JSON IR of the input tuple’s IR version.

Without an agreed generateModel tuple, the request is -32013. A model that the generator rejects, or a model that is not a Library (such as a Specs model), gives a result with success: false, error diagnostics and no artifacts. A successful result has one text artifact for each generated file, in the generator’s dependency order, the order in which the built-in backend lists its files. Each path is relative to the output directory, with / as the separator; the guest checks that it has no empty, . or .. segment and no \ or :, and returns -32603 instead of an artifact that fails the check. The guest never writes files; it returns them to the host.

By default the text is formatted by Fantomas, as in dotnet-morphir, so its line endings are those of the platform the guest runs on: CRLF on Windows, LF elsewhere. With the option format false, the guest writes the generator’s own text, byte-identical to the built-in backend with format=false.

Generation runs beside the session: while a generateModel runs, the guest reads and answers other messages, such as morphir.ping and morphir.shutdown. The generator cannot stop part way. The guest checks for a cancelled session (for example Ctrl+C) before and after it generates, and then answers -32800. It does not claim cancellation, so it does not act on $/cancelRequest and serves a request that a host asks to cancel.

The guest makes no trimming or Native AOT claim. It is not published as a self-contained executable yet.

How the extension is checked

The tests in tests/Morphir.CodeGeneration.FSharp.Extension.Tests start the guest as a process behind the Morphir.Mep host session. They check the claim set, the negotiated tuples, and that an unagreed tuple is refused and not served. They compare the guest’s files (paths, order and text) with those of the managed built-in backend, through FSharpGenerationBackend, on the same machine, for these checked-in models: the daily-rate models at IR 3 and IR 4 with default options, and the two-module constant-folding models at IR 3 and IR 4 with optimize true and with optimize false. For the constant-folding models they also check that the two settings give different text. There is no comparison with the Native AOT morphir, whose files are unformatted. Other tests check malformed params, a model nested just under the agreed maxDepth, generation beside a ping, and the path check. A further test packs and installs the tool and reads its claims.

The host side, MepGenerationBackend in Morphir.Tooling, is checked against the real guest with the same comparison, and against scripted guests for each failure code. morphir generate --extension and [codegen.fsharp].extension are checked through the command line. Because the files come from the guest, the Native AOT morphir with --extension publishes the guest’s formatted files, not the unformatted files of its built-in backend.

Unformatted output

--option format=false turns Fantomas formatting off, for the built-in backend and for the extension alike. The files are the generator’s own text:

  • LF line endings only, and exactly one final LF;
  • UTF-8 without a byte order mark;
  • the same bytes on every operating system, under JIT and Native AOT, and in every current culture.

For the built-in backend the generate report says "formatting": "off"; the report of the extension has no formatting member, because the host cannot observe what the guest did. The default formats when the host can run Fantomas ("applied"); the Native AOT morphir cannot, so under the default it writes the same unformatted text, reports "unavailable" and warns on stderr. Formatting the generated files with a tool of your choice around generate is planned separately.

The Morphir compatibility check compares this unformatted set. For the finos/morphir case fsharp-compile-generate, the built-in backend (JIT host, Native AOT host and dotnet-morphir) and the extension (driven by the JIT and by the Native AOT host) give the same single file, Pricing.fs, 112 bytes. The test project pins those bytes, and its compile step (the case’s F# source to its expected IR) runs through the compatibility kit.

The case’s expected generated file is frozen in the contract pinned at finos/morphir 9e1f853b7: its case.json has "status": "frozen" and "formatting": "off", and names expected/generated/Pricing.fs with those 112 bytes and SHA-256 64e08313f3e57be2c18995a2e8b275db9d921341958da2c8cd960c601c066de0. This repository now includes that pinned file under tests/Fixtures/Toolchain/workflows/fsharp-compile-generate/; the tests compare the generated size, digest and bytes with it. The recorded five-delivery observation was on Windows ARM64 using local builds, not released packages. The guest has not been qualified for generation through the shared compatibility runner. The compile-step test uses the .NET library adapter; the in-repository runner has no generate observation or file-set comparison. morphir mck routes to a separately installed kit and does not establish that qualification. See deviations.

23 - Project-file sources

Name a build-tool project file as the ordered sources of a compile

Project-file sources

A build tool’s project file can give the sources of morphir compile and their order. The command expands the project file on the host into an ordered source set: a root (the directory of the project file) and documents relative to that root. Frontends receive plain ordered documents. They do not know that a project file existed.

Name the project file

Use one of these. The command reads only the first that applies:

  1. --project-file <file> on the command line;
  2. project_file in the [frontend.<language>] table of the selected language.
[frontend]
language = "fsharp"

[frontend.fsharp]
project_file = "src/Acme.CarRental.fsproj"

Every [frontend.<language>] table accepts the same project_file key, with the same meaning as --project-file. A relative path is relative to the directory of the file that sets it. The command reads the key only from the table of the selected language (--language, else frontend.language). The key of another language’s table is not read. If no language is selected, no project_file key is read; if no other source selection then applies, the command fails with compile.language_required.

--input or --project-file on the command line replaces a configured project_file for that run, and the configured project file is not evaluated. --project-file and --input together fail with sources.conflicting_source_options (exit code 2). An --input file that a provider recognizes as a project file fails with sources.project_file_as_input (exit code 2); use --project-file for it.

The command evaluates a project file only when you name it. It never searches directories for project files. A project file in the source directory that no option or key names is not read.

Providers

A project-file provider expands one kind of project file. Each provider declares file-name patterns, and the command gives the project file to the one provider whose pattern matches it. Providers are selected by the project file, not by the language.

CodeExitMeaning
sources.unknown_project_file2No provider recognizes the file. The message names the file and the registered patterns.
sources.ambiguous_project_file2More than one provider recognizes the file. The message names them.
sources.missing2The project file does not exist.

The MSBuild provider

The MSBuild provider recognizes MSBuild project files (*.*proj), for example .fsproj and .csproj. It reads the Compile, ProjectReference and PackageReference items through project evaluation only. It does not restore, build or run targets, and it writes no output: no obj/ or bin/ directory.

Both executables, morphir and dotnet-morphir, have this provider.

The provider runs this command as a child process, with explicit arguments, no shell, and a deadline. It uses DOTNET_HOST_PATH when that is set, else dotnet on PATH:

dotnet msbuild <file> -getItem:Compile -getItem:ProjectReference -getItem:PackageReference
  -getProperty:MSBuildProjectDirectory -getProperty:MSBuildExtensionsPath
  -nologo -nodeReuse:false -maxCpuCount:1 -noAutoResponse -interactive:false

with MSBUILDDISABLENODEREUSE=1, DOTNET_CLI_TELEMETRY_OPTOUT=1, DOTNET_NOLOGO=1 and DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1. No build process stays running after the command ends.

Requirements: a .NET SDK 8 or later on PATH. Without one, the command fails with msbuild.unavailable (exit code 2), and the message names the required SDK.

The documents are the Compile items, in the order the project declares them. A .csproj expands the same way. Frontend selection, not the provider, then decides whether the language is supported.

Project contentResult
<Compile Include="Rates.fs" />A document, in declared order.
A Compile item outside the project directory, for example <Compile Include="../shared/Common.fs" Link="Common.fs" />sources.outside_root (exit code 2), naming ../shared/Common.fs. Nothing is compiled.
ProjectReferencemsbuild.unsupported_project_reference (exit code 2).
PackageReferenceThe warning msbuild.ignored_package_reference, naming the package. Compilation continues.
An evaluation errormsbuild.evaluation_failed (exit code 2), with the build tool’s message.
Evaluation that does not end before the deadlinemsbuild.deadline_exceeded (exit code 2). The provider stops the build tool and its child processes.
Evaluation output larger than the limitmsbuild.output_limit_exceeded (exit code 2).

Example:

dotnet-morphir compile --project-file src/Acme.CarRental.fsproj --package-name acme/car-rental --json

For a project that declares Rates.fs then Pricing.fs, the report has "sources": { "projectFile": "src/Acme.CarRental.fsproj", "root": "src", "documents": ["Rates.fs", "Pricing.fs"] }.

Evaluation runs project logic

MSBuild evaluation can run project logic: imports and property functions. That is why a provider runs only on a file that you name. Name only project files that you trust.

Expanded documents follow the source rules

The command reads expanded documents with the same rules as files that you name: strict UTF-8, links and special files refused, and byte, document and path limits. A Compile item that is a symbolic link fails before compilation with a diagnostic that names the document.

See also