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 vocabulary
Public C# API
Maybe.Just, Nothing
Maybe<T>.Just(value), Maybe<T>.Nothing
Maybe.map, andThen
value.Map(callback), value.AndThen(callback)
Result.Ok, Err
Result<TError,TValue>.Ok(value), .Err(error)
Result.map, andThen
result.Map(callback), result.AndThen(callback)
Unit; Basics.LT, EQ, GT
Unit.Value; Order.LT, .EQ, .GT, also Basics constants
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.
sealedrecordAmount(double Number, Morphir.SDK.CSharp.List<int> Values)
: IMorphirValue
{
publicbool MorphirEquals(IMorphirValue other) => other is Amount x
&& Basics.Equal(Number, x.Number) && Basics.Equal(Values, x.Values);
publicint MorphirHashCode() => MorphirValue.Hash((Number, Values));
publicbool Equals(Amount? other) => other is not null && MorphirEquals(other);
publicoverrideint 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.
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:
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
Compile F# to Morphir IR: a larger rule, compiled with the
.NET tool dotnet-morphir, and C# generated from it.
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.
Platform
Supported
Windows x64 and ARM64
Yes
macOS on Apple silicon
Yes
Linux x64 and ARM64
morphirup 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.
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:
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.
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.
"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 =typeCar={ 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:
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:
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
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:
Machine
File
macOS, Apple silicon
morphirup-osx-arm64
Linux x64
morphirup-linux-x64
Linux ARM64
morphirup-linux-arm64
Windows x64
morphirup-win-x64.exe
Windows ARM64
morphirup-win-arm64.exe
Check the file before you run it. On macOS and Linux, in the download folder (on Linux, sha256sum -c - works too):
It prints True. Rename the file to morphirup.exe and move it into a folder on your PATH.
What works on each platform
Platform
Install morphirup
Set up, compile and generate
macOS on Apple silicon
Yes
Yes
Windows x64 and ARM64
Yes
Yes
Linux x64 and ARM64
Yes
Not yet: morphirup setup does not support Linux yet
macOS on Intel
No
No
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.
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):
Offering
To run it
To install it
Released
Native AOT morphir
Nothing from .NET
Nothing
Not yet as this offering
Self-contained morphir (carries its own .NET runtime)
Nothing from .NET
Nothing
Not yet
Framework-dependent morphir
The .NET runtime Microsoft.NETCore.App 11.x
Nothing
Not yet
dotnet-morphir (Morphir.Tool dotnet tool)
The .NET runtime Microsoft.NETCore.App 11.x
A .NET SDK 11.0.100 or later, for dotnet tool install
Yes, 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:
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:
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.
Platform
Self-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 ARM64
Not 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:
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).
Offering
Artifact
Needs on the machine
F#
native-aot
morphir-dotnet-<release>-native-aot-<rid>.zip or .tar.gz
Nothing from .NET
Through the F# frontend extension (--extension)
self-contained
morphir-dotnet-<release>-self-contained-<rid>.zip or .tar.gz
Nothing from .NET
Built in
framework-dependent
morphir-dotnet-<release>-framework-dependent-<rid>.zip or .tar.gz
The .NET 10 runtime (Microsoft.NETCore.App 10.0) for the same architecture
Built in
dotnet-tool
Morphir.Tool.<release>.nupkg, as this build packs it
The .NET SDK 10.0 to install it, and the .NET 10 runtime to run it
Built 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.
/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:
Install the .NET 10 runtime for the architecture that the message names, from the download link it prints.
If .NET is installed in a folder of your own, set DOTNET_ROOT (or DOTNET_ROOT_ARM64, DOTNET_ROOT_X64) to
that folder.
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]
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.
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.
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
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
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.
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.
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:
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.
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:
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.
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.
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.
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:
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.
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:
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.
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:
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:
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:
Cause
Message
The file is not complete JSON
Error: malformed-json: ...\broken.json: The input is not a complete JSON document.
formatVersion is a string, "3", not 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.
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:
using Pricing.Generated;
var cents = InventoryModule.DailyRate();
if (cents != 5000) thrownew System.Exception("Unexpected daily rate");
System.Console.WriteLine(cents);
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.
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:
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.
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:
Use Records: Prefer record types for immutable data structures
Pattern Matching: Use exhaustive pattern matching for ADTs
Validation: Implement smart constructors for validated types
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);
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;
publicclassTypeExprTests{
[Test]publicvoid 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]publicbool 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]publicvoid Should_Roundtrip_With_Morphir_Elm()
{
// Load canonical IR samplevar json = File.ReadAllText("samples/canonical.json");
var ir = JsonSerializer.Deserialize<IR>(json);
// Serialize backvar roundtrip = JsonSerializer.Serialize(ir);
// Verify compatibility Assert.That(roundtrip).IsValidJson();
}
Best Practices
Exhaustive Testing: Test all ADT cases
Edge Cases: Test boundary conditions
Roundtrip Tests: Always test serialization roundtrips
Property Tests: Use property-based testing for invariants
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]
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 =typeVehicleClass=| 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 =typeRental={ 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:
<CompileInclude="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
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).
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
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.
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.
Setting
Meaning
MorphirModel item
One classic library JSON input
MorphirModel.OutputDirectory
Optional model-owned output directory, relative to the project directory or absolute
MorphirMyriadHostPath
Optional 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:
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:
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 SDK
Generated representation
Basics.Int, Basics.Float, Basics.Bool
int, float, bool
String.String, Char.Char
string, char
List.List a
a list
Maybe.Maybe a, Just, Nothing
a option, Some, None
Result.Result error value, Ok, Err
Result<value, error>, Ok, Error
Basics.Order and its cases
Morphir.SDK.Order
Addition, subtraction, multiplication, division
Corresponding curried F# operators
Equality and ordered comparisons
Corresponding F# comparisons
List.map, Maybe.map, Result.map
List.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
Symptom
Corrective step
Missing model path
Correct MorphirModel.Include relative to the consumer project
unsupported-version
Supply explicit classic integer version 1, 2 or 3
Decode error
Check the reported location against that version’s wire shape
Open-record diagnostic
Replace the unsupported open row with a closed model type
Module dependency cycle
Refactor the model’s cross-module cycle; files remain separate
Unmapped SDK/external symbol
Use a supported mapping or provide an implementation in the model
unbound-variable
Bind the variable in the named value, or correct its name
type-mismatch
Correct the named value. Only definite conflicts between primitive types are reported, such as a string literal under a declared Int
fsharp-generation
Review the rendering reason and IR declaration; constructs without an F# representation, such as empty unions, fail without partial output
Duplicate output directory or path
Assign distinct model ownership and names
Host method/assembly mismatch
Use the documented pinned packages and automatic isolated host setup
Old plugin code after repacking
Use 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
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
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:
(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
Code
Fix
extension.unavailable
Install the extension, or check that it is on PATH.
extension.not_admitted
Request an IR version that the extension serves (1 to 4).
extension.protocol_error
Update the extension.
extension.timeout
Read 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.
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:
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.
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:
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.
The JSON name. More names are read; the first is written. On a case, Field = "x" names the field x
JsonPropertyName
Field
The System.Text.Json attribute, for one name
JsonParsedValue
Type, abbreviation
A value that is written as text and read only through its parser
JsonIgnoreType
Type
Leave the type out of the context
JsonDefaultValue
Record field
Use a string, Boolean or numeric literal when the member is absent
JsonDefaultEmpty
Collection field
Use the empty collection when the member is absent
JsonDefaultValueProvider
Record field
Call an accessible static method with no arguments when the member is absent
JsonDefaultJson
Record field
Read an explicit JSON literal through the generated field codec when absent
JsonOmitEmpty
Record collection field
Omit an empty collection on writing
JsonOmitDefault
Record bool field with JsonDefaultValue
Omit the value on writing when it equals the default
JsonExtensionData
Record map field
Capture unknown members and write them beside the named members. This is the System.Text.Json attribute
JsonVersionAlias
Record version field
Normalize an exact legacy member/version pair to the current member and version
JsonKnownPaths
Record string collection
Accept only the declared exact critical paths
JsonConverter
Type, field
The System.Text.Json attribute: use this converter
JsonNumberHandling
Number field
The 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)>]typeSettings={ 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:
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:
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:
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
Encoding
NoArgs
OneArg 1
TwoArgs(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"}
Flag
Effect
UnwrapFieldlessTags
A case with no field is the string of its name: "NoArgs"
UnwrapSingleFieldCases
With AdjacentTag or ExternalTag, one field is written alone: {"OneArg":1}
UnwrapRecordCases
The members of a case’s single record field are written in place of the field
UnionFieldNamesFromTypes
A field with no name is named after its type: Int32, String, Point
UnwrapOption
None is null and Some x is x. Without it, Some x is a union case
UnwrapSingleCaseUnions
A union with one case and one field is written as that field
AllowUnorderedTag
When reading, the case member can come after the fields
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")>]typeShape=| 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")>]typeEmail=private| Email ofstringmember this.Value=let(Email value)= this in value
staticmember 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))>]typeRequestId=| Number ofuint64| Text ofstring
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
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.
Replace JsonFSharpConverter attributes with JsonUnion or JsonRecord. JsonName keeps its meaning.
Replace JsonSerializer.Serialize(value, options) with the type info overload, JsonSerializer.Serialize(value, AppJson.Default.Person), or set TypeInfoResolver on your options.
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
Module
Business responsibility
Domain
Immutable vehicle and invoice records, lifecycle unions and typed errors
Inventory
Sample car, daily rate, lookup and availability
Reservation
Reserve a car and capture its daily rate
Pickup
Start the rental from a confirmed reservation
Return
Record billable days and release the car
Billing
Produce one invoice after return
Workflow
Compose 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.
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.
Operation
Rental status
Inventory availability
Initial inventory
Unreserved
Available
Reserve
Confirmed 5000
Reserved
Pick up
Active 5000
Rented
Return after three days
Completed (5000, 3)
Available
Bill
Invoiced invoice
Available
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:
Attempt
Typed business error
Reserve the reserved car
Unavailable
Pick up the initial car
ReservationRequired
Return the initial car
ActiveRentalRequired
Reserve for zero days
InvalidDuration
Return for zero billable days
InvalidDuration
Bill before return
ReturnRequired
Bill an already billed rental
AlreadyBilled
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:
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
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.
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:
Within expressions: References use FQName (package, module, local name)
Within modules: Items use local Names (looked up in module context)
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:
Preserve names in canonical form: Convert all identifiers to lowercase word lists
Use fully-qualified references: Always include package and module paths
Maintain access control: Mark public vs private appropriately
Extract lambdas into function parameters: Use the inputTypes field instead of nested lambdas where possible
Preserve documentation: Include doc strings from source
Consuming IR
When consuming Morphir IR:
Respect access control: Only access public items from dependencies
Resolve references: Use the distribution to look up type and value definitions
Handle attributes: Be prepared for different attribute types or use unit type
Follow naming conventions: Use Name conversion utilities for target platform
Process hierarchically: Start from Distribution → Package → Module → Types/Values
Transforming IR
When transforming Morphir IR:
Preserve structure: Maintain the hierarchical organization
Update references consistently: If you rename items, update all references
Maintain type correctness: Ensure transformations preserve type safety
Handle both specifications and definitions: Transform both forms consistently
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:
Validation: Validate Morphir IR JSON files against the appropriate version schema
Documentation: Understand the structure and constraints of the IR format
Code Generation: Generate parsers, serializers, and type definitions for various languages
Tooling: Build editors, linters, and other tools that work with Morphir IR
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).
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
For a quick check without full validation, you can verify basic structure:
import json
defcheck_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) ==4assert 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:
Generate Code: Create type definitions and parsers for various programming languages
IDE Support: Provide autocomplete and validation in JSON editors
Testing: Validate generated IR in test suites
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:
Update the appropriate schema file(s) to match the upstream schemas from the main Morphir repository
Update the format version handling in the .NET codec implementation if needed
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
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:
Capitalize all value tags
Capitalize all pattern tags
Capitalize all literal tags
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.
# 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: objectrequired:
- formatVersion - distributionproperties:
formatVersion:
type: integerconst: 3description: "The version of the IR format. Must be 3 for this schema."distribution:
description: "The distribution data, currently only Library distributions are supported."type: arrayminItems: 4maxItems: 4items:
- type: stringconst: "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: arrayitems:
type: stringpattern: "^[a-z][a-z0-9]*$"minItems: 1description: | 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: arrayitems:
$ref: "#/definitions/Name"minItems: 1description: | 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: arrayminItems: 3maxItems: 3items:
- $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: objectdescription: | 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: objectrequired: ["access", "value"]
properties:
access:
type: stringenum: ["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: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/PackageName" - $ref: "#/definitions/PackageSpecification"description: "Dictionary of package dependencies, contains only type signatures."PackageDefinition:
type: objectrequired: ["modules"]
properties:
modules:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: objectrequired: ["modules"]
properties:
modules:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/ModuleName" - $ref: "#/definitions/ModuleSpecification"description: "Public modules only."description: "Public interface of a package, contains only type signatures."# ===== Module Structure =====ModuleDefinition:
type: objectrequired: ["types", "values"]
properties:
types:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - allOf:
- $ref: "#/definitions/AccessControlled" - properties:
value:
# Documented wrapper: can have "doc" and "value", or just the type definition directlyoneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/TypeDefinition" - $ref: "#/definitions/TypeDefinition"description: "All type definitions (public and private)."values:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - allOf:
- $ref: "#/definitions/AccessControlled" - properties:
value:
# Documented wrapper: can have "doc" and "value", or just the value definition directlyoneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/ValueDefinition" - $ref: "#/definitions/ValueDefinition"description: "All value definitions (public and private)."doc:
type: stringdescription: "Optional documentation for the module."description: "Complete implementation of a module."ModuleSpecification:
type: objectrequired: ["types", "values"]
properties:
types:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - oneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/TypeSpecification" - $ref: "#/definitions/TypeSpecification"description: "Public type specifications only."values:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - oneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/ValueSpecification" - $ref: "#/definitions/ValueSpecification"description: "Public value specifications only."doc:
type: stringdescription: "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: arrayminItems: 3maxItems: 3items:
- const: "Variable" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name"description: "Represents a type variable (generic parameter)."ReferenceType:
type: arrayminItems: 4maxItems: 4items:
- const: "Reference" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName" - type: arrayitems:
$ref: "#/definitions/Type"description: "Type arguments for generic types."description: "Reference to another type or type alias."TupleType:
type: arrayminItems: 3maxItems: 3items:
- const: "Tuple" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Type"description: "Element types in order."description: "A composition of multiple types in a fixed order (product type)."RecordType:
type: arrayminItems: 3maxItems: 3items:
- const: "Record" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Field"description: "List of field definitions."description: "A composition of named fields with their types."ExtensibleRecordType:
type: arrayminItems: 4maxItems: 4items:
- const: "ExtensibleRecord" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name" - type: arrayitems:
$ref: "#/definitions/Field"description: "Known fields."description: "A record type that can be extended with additional fields."FunctionType:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 2maxItems: 2items:
- const: "Unit" - $ref: "#/definitions/Attributes"description: "The type with exactly one value (similar to void in some languages)."Field:
type: objectrequired: ["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: arrayminItems: 3maxItems: 3items:
- const: "TypeAliasSpecification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - $ref: "#/definitions/Type"description: "An alias for another type."OpaqueTypeSpecification:
type: arrayminItems: 2maxItems: 2items:
- const: "OpaqueTypeSpecification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters."description: | A type with unknown structure. The implementation is hidden from consumers.CustomTypeSpecification:
type: arrayminItems: 3maxItems: 3items:
- const: "CustomTypeSpecification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - $ref: "#/definitions/Constructors"description: "A tagged union type (sum type)."DerivedTypeSpecification:
type: arrayminItems: 3maxItems: 3items:
- const: "DerivedTypeSpecification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - type: objectrequired: ["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: arrayminItems: 3maxItems: 3items:
- const: "TypeAliasDefinition" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - $ref: "#/definitions/Type"description: "Complete definition of a type alias."CustomTypeDefinition:
type: arrayminItems: 3maxItems: 3items:
- const: "CustomTypeDefinition" - type: arrayitems:
$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: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: arrayminItems: 3maxItems: 3items:
- const: "Literal" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Literal"description: "A literal constant value."ConstructorValue:
type: arrayminItems: 3maxItems: 3items:
- const: "Constructor" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName"description: "Reference to a custom type constructor."TupleValue:
type: arrayminItems: 3maxItems: 3items:
- const: "Tuple" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Value"description: "Element values in order."description: "A tuple value with multiple elements."ListValue:
type: arrayminItems: 3maxItems: 3items:
- const: "List" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Value"description: "List elements."description: "A list of values."RecordValue:
type: arrayminItems: 3maxItems: 3items:
- const: "Record" - $ref: "#/definitions/Attributes" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - $ref: "#/definitions/Value"description: "Dictionary mapping field names to values."description: "A record value with named fields."VariableValue:
type: arrayminItems: 3maxItems: 3items:
- const: "Variable" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name"description: "Reference to a variable in scope."ReferenceValue:
type: arrayminItems: 3maxItems: 3items:
- const: "Reference" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName"description: "Reference to a defined value (function or constant)."FieldValue:
type: arrayminItems: 4maxItems: 4items:
- const: "Field" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Value" - $ref: "#/definitions/Name"description: "Field access on a record. Items: [tag, attributes, recordExpr, fieldName]"FieldFunctionValue:
type: arrayminItems: 3maxItems: 3items:
- const: "FieldFunction" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name"description: "A function that extracts a field (e.g., .firstName)."ApplyValue:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 4maxItems: 4items:
- const: "Lambda" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Pattern" - $ref: "#/definitions/Value"description: | Anonymous function (lambda abstraction).
Items: [tag, attributes, argumentPattern, body]LetDefinitionValue:
type: arrayminItems: 5maxItems: 5items:
- 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: arrayminItems: 4maxItems: 4items:
- const: "LetRecursion" - $ref: "#/definitions/Attributes" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - $ref: "#/definitions/ValueDefinition"description: "Multiple bindings that can reference each other." - $ref: "#/definitions/Value"description: "Mutually recursive let bindings."DestructureValue:
type: arrayminItems: 5maxItems: 5items:
- 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: arrayminItems: 5maxItems: 5items:
- 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: arrayminItems: 4maxItems: 4items:
- const: "PatternMatch" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Value" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Pattern" - $ref: "#/definitions/Value"description: "List of pattern-branch pairs."description: "Pattern matching with multiple cases."UpdateRecordValue:
type: arrayminItems: 4maxItems: 4items:
- const: "UpdateRecord" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Value" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: arrayminItems: 2maxItems: 2items:
- 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: arrayminItems: 2maxItems: 2items:
- const: "BoolLiteral" - type: booleandescription: "Boolean literal (true or false)."CharLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "CharLiteral" - type: stringminLength: 1maxLength: 1description: "Single character literal."StringLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "StringLiteral" - type: stringdescription: "Text string literal."WholeNumberLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "WholeNumberLiteral" - type: integerdescription: "Integer literal."FloatLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "FloatLiteral" - type: numberdescription: "Floating-point number literal."DecimalLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "DecimalLiteral" - type: stringpattern: "^-?[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: arrayminItems: 2maxItems: 2items:
- const: "WildcardPattern" - $ref: "#/definitions/Attributes"description: "Matches any value without binding (the _ pattern)."AsPattern:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 3maxItems: 3items:
- const: "TuplePattern" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Pattern"description: "Patterns for each tuple element."description: "Matches a tuple by matching each element."ConstructorPattern:
type: arrayminItems: 4maxItems: 4items:
- const: "ConstructorPattern" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName" - type: arrayitems:
$ref: "#/definitions/Pattern"description: "Patterns for constructor arguments."description: "Matches a specific type constructor and its arguments."EmptyListPattern:
type: arrayminItems: 2maxItems: 2items:
- const: "EmptyListPattern" - $ref: "#/definitions/Attributes"description: "Matches an empty list (the [] pattern)."HeadTailPattern:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 3maxItems: 3items:
- const: "LiteralPattern" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Literal"description: "Matches an exact literal value."UnitPattern:
type: arrayminItems: 2maxItems: 2items:
- const: "UnitPattern" - $ref: "#/definitions/Attributes"description: "Matches the unit value."# ===== Value Specifications and Definitions =====ValueSpecification:
type: objectrequired: ["inputs", "output"]
properties:
inputs:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: objectrequired: ["inputTypes", "outputType", "body"]
properties:
inputTypes:
type: arrayitems:
type: arrayminItems: 3maxItems: 3items:
- $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:
Update module structure: Convert {"name": ..., "def": ...} to array format
Capitalize all type tags: "variable" → "Variable", "reference" → "Reference", etc.
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.
# 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: objectrequired:
- formatVersion - distributionproperties:
formatVersion:
type: integerconst: 2description: "The version of the IR format. Must be 2 for this schema."distribution:
description: "The distribution data, currently only Library distributions are supported."type: arrayminItems: 4maxItems: 4items:
- type: stringconst: "Library"description: "Distribution type (capitalized in v2)." - $ref: "#/definitions/PackageName" - $ref: "#/definitions/Dependencies" - $ref: "#/definitions/PackageDefinition"definitions:
# ===== Basic Building Blocks =====Name:
type: arrayitems:
type: stringpattern: "^[a-z][a-z0-9]*$"minItems: 1description: | 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: arrayitems:
$ref: "#/definitions/Name"minItems: 1description: | 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: arrayminItems: 3maxItems: 3items:
- $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: objectdescription: | 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: objectrequired: ["access", "value"]
properties:
access:
type: stringenum: ["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: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/PackageName" - $ref: "#/definitions/PackageSpecification"description: "Dictionary of package dependencies, contains only type signatures."PackageDefinition:
type: objectrequired: ["modules"]
properties:
modules:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: objectrequired: ["modules"]
properties:
modules:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/ModuleName" - $ref: "#/definitions/ModuleSpecification"description: "Public modules only."description: "Public interface of a package, contains only type signatures."# ===== Module Structure =====ModuleDefinition:
type: objectrequired: ["types", "values"]
properties:
types:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - allOf:
- $ref: "#/definitions/AccessControlled" - properties:
value:
# Documented wrapper: can have "doc" and "value", or just the type definition directlyoneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/TypeDefinition" - $ref: "#/definitions/TypeDefinition"description: "All type definitions (public and private)."values:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - allOf:
- $ref: "#/definitions/AccessControlled" - properties:
value:
# Documented wrapper: can have "doc" and "value", or just the value definition directlyoneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/ValueDefinition" - $ref: "#/definitions/ValueDefinition"description: "All value definitions (public and private)."doc:
type: stringdescription: "Optional documentation for the module."description: "Complete implementation of a module."ModuleSpecification:
type: objectrequired: ["types", "values"]
properties:
types:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - oneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/TypeSpecification" - $ref: "#/definitions/TypeSpecification"description: "Public type specifications only."values:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - oneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/ValueSpecification" - $ref: "#/definitions/ValueSpecification"description: "Public value specifications only."doc:
type: stringdescription: "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: arrayminItems: 3maxItems: 3items:
- const: "Variable" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name"description: "Represents a type variable (generic parameter)."ReferenceType:
type: arrayminItems: 4maxItems: 4items:
- const: "Reference" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName" - type: arrayitems:
$ref: "#/definitions/Type"description: "Type arguments for generic types."description: "Reference to another type or type alias."TupleType:
type: arrayminItems: 3maxItems: 3items:
- const: "Tuple" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Type"description: "Element types in order."description: "A composition of multiple types in a fixed order (product type)."RecordType:
type: arrayminItems: 3maxItems: 3items:
- const: "Record" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Field"description: "List of field definitions."description: "A composition of named fields with their types."ExtensibleRecordType:
type: arrayminItems: 4maxItems: 4items:
- const: "ExtensibleRecord" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name" - type: arrayitems:
$ref: "#/definitions/Field"description: "Known fields."description: "A record type that can be extended with additional fields."FunctionType:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 2maxItems: 2items:
- const: "Unit" - $ref: "#/definitions/Attributes"description: "The type with exactly one value (similar to void in some languages)."Field:
type: objectrequired: ["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: arrayminItems: 3maxItems: 3items:
- const: "TypeAliasSpecification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - $ref: "#/definitions/Type"description: "An alias for another type."OpaqueTypeSpecification:
type: arrayminItems: 2maxItems: 2items:
- const: "OpaqueTypeSpecification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters."description: | A type with unknown structure. The implementation is hidden from consumers.CustomTypeSpecification:
type: arrayminItems: 3maxItems: 3items:
- const: "CustomTypeSpecification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - $ref: "#/definitions/Constructors"description: "A tagged union type (sum type)."DerivedTypeSpecification:
type: arrayminItems: 3maxItems: 3items:
- const: "DerivedTypeSpecification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - type: objectrequired: ["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: arrayminItems: 3maxItems: 3items:
- const: "TypeAliasDefinition" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - $ref: "#/definitions/Type"description: "Complete definition of a type alias."CustomTypeDefinition:
type: arrayminItems: 3maxItems: 3items:
- const: "CustomTypeDefinition" - type: arrayitems:
$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: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: arrayminItems: 3maxItems: 3items:
- const: "literal" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Literal"description: "A literal constant value."ConstructorValue:
type: arrayminItems: 3maxItems: 3items:
- const: "constructor" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName"description: "Reference to a custom type constructor."TupleValue:
type: arrayminItems: 3maxItems: 3items:
- const: "tuple" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Value"description: "Element values in order."description: "A tuple value with multiple elements."ListValue:
type: arrayminItems: 3maxItems: 3items:
- const: "list" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Value"description: "List elements."description: "A list of values."RecordValue:
type: arrayminItems: 3maxItems: 3items:
- const: "record" - $ref: "#/definitions/Attributes" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - $ref: "#/definitions/Value"description: "Dictionary mapping field names to values."description: "A record value with named fields."VariableValue:
type: arrayminItems: 3maxItems: 3items:
- const: "variable" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name"description: "Reference to a variable in scope."ReferenceValue:
type: arrayminItems: 3maxItems: 3items:
- const: "reference" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName"description: "Reference to a defined value (function or constant)."FieldValue:
type: arrayminItems: 4maxItems: 4items:
- const: "field" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Value" - $ref: "#/definitions/Name"description: "Field access on a record. Items: [tag, attributes, recordExpr, fieldName]"FieldFunctionValue:
type: arrayminItems: 3maxItems: 3items:
- const: "field_function" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name"description: "A function that extracts a field (e.g., .firstName)."ApplyValue:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 4maxItems: 4items:
- const: "lambda" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Pattern" - $ref: "#/definitions/Value"description: | Anonymous function (lambda abstraction).
Items: [tag, attributes, argumentPattern, body]LetDefinitionValue:
type: arrayminItems: 5maxItems: 5items:
- 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: arrayminItems: 4maxItems: 4items:
- const: "let_recursion" - $ref: "#/definitions/Attributes" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - $ref: "#/definitions/ValueDefinition"description: "Multiple bindings that can reference each other." - $ref: "#/definitions/Value"description: "Mutually recursive let bindings."DestructureValue:
type: arrayminItems: 5maxItems: 5items:
- 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: arrayminItems: 5maxItems: 5items:
- 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: arrayminItems: 4maxItems: 4items:
- const: "pattern_match" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Value" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Pattern" - $ref: "#/definitions/Value"description: "List of pattern-branch pairs."description: "Pattern matching with multiple cases."UpdateRecordValue:
type: arrayminItems: 4maxItems: 4items:
- const: "update_record" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Value" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: arrayminItems: 2maxItems: 2items:
- 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: arrayminItems: 2maxItems: 2items:
- const: "bool_literal" - type: booleandescription: "Boolean literal (true or false)."CharLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "char_literal" - type: stringminLength: 1maxLength: 1description: "Single character literal."StringLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "string_literal" - type: stringdescription: "Text string literal."WholeNumberLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "whole_number_literal" - type: integerdescription: "Integer literal."FloatLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "float_literal" - type: numberdescription: "Floating-point number literal."DecimalLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "decimal_literal" - type: stringpattern: "^-?[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: arrayminItems: 2maxItems: 2items:
- const: "wildcard_pattern" - $ref: "#/definitions/Attributes"description: "Matches any value without binding (the _ pattern)."AsPattern:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 3maxItems: 3items:
- const: "tuple_pattern" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Pattern"description: "Patterns for each tuple element."description: "Matches a tuple by matching each element."ConstructorPattern:
type: arrayminItems: 4maxItems: 4items:
- const: "constructor_pattern" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName" - type: arrayitems:
$ref: "#/definitions/Pattern"description: "Patterns for constructor arguments."description: "Matches a specific type constructor and its arguments."EmptyListPattern:
type: arrayminItems: 2maxItems: 2items:
- const: "empty_list_pattern" - $ref: "#/definitions/Attributes"description: "Matches an empty list (the [] pattern)."HeadTailPattern:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 3maxItems: 3items:
- const: "literal_pattern" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Literal"description: "Matches an exact literal value."UnitPattern:
type: arrayminItems: 2maxItems: 2items:
- const: "unit_pattern" - $ref: "#/definitions/Attributes"description: "Matches the unit value."# ===== Value Specifications and Definitions =====ValueSpecification:
type: objectrequired: ["inputs", "output"]
properties:
inputs:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: objectrequired: ["inputTypes", "outputType", "body"]
properties:
inputTypes:
type: arrayitems:
type: arrayminItems: 3maxItems: 3items:
- $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:
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: arrayitems:
type: stringpattern: "^[a-z][a-z0-9]*$"minItems: 1description: | 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: arrayitems:
$ref: "#/definitions/Name"minItems: 1description: | 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
# 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: objectrequired:
- formatVersion - distributionproperties:
formatVersion:
type: integerconst: 1description: "The version of the IR format. Must be 1 for this schema."distribution:
description: "The distribution data, currently only Library distributions are supported."type: arrayminItems: 4maxItems: 4items:
- type: stringconst: "library"description: "Distribution type (lowercase in v1)." - $ref: "#/definitions/PackageName" - $ref: "#/definitions/Dependencies" - $ref: "#/definitions/PackageDefinition"definitions:
# ===== Basic Building Blocks =====Name:
type: arrayitems:
type: stringpattern: "^[a-z][a-z0-9]*$"minItems: 1description: | 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: arrayitems:
$ref: "#/definitions/Name"minItems: 1description: | 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: arrayminItems: 3maxItems: 3items:
- $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: objectdescription: | 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: stringenum: ["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: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/PackageName" - $ref: "#/definitions/PackageSpecification"description: "Dictionary of package dependencies, contains only type signatures."PackageDefinition:
type: objectrequired: ["modules"]
properties:
modules:
type: arrayitems:
$ref: "#/definitions/ModuleEntry"description: "All modules in the package (public and private)."description: "Complete implementation of a package with all details."ModuleEntry:
type: objectrequired: ["name", "def"]
properties:
name:
$ref: "#/definitions/ModuleName"description: "The module name/path."def:
type: arrayminItems: 2maxItems: 2items:
- $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: objectrequired: ["modules"]
properties:
modules:
type: arrayitems:
type: objectrequired: ["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: objectrequired: ["types", "values"]
properties:
types:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/AccessLevel" - oneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/TypeDefinition" - $ref: "#/definitions/TypeDefinition"description: "All type definitions (public and private)."values:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/AccessLevel" - oneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/ValueDefinition" - $ref: "#/definitions/ValueDefinition"description: "All value definitions (public and private)."doc:
type: stringdescription: "Optional documentation for the module."description: "Complete implementation of a module."ModuleSpecification:
type: objectrequired: ["types", "values"]
properties:
types:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - oneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/TypeSpecification" - $ref: "#/definitions/TypeSpecification"description: "Public type specifications only."values:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - oneOf:
- type: objectrequired: ["doc", "value"]
properties:
doc:
type: stringvalue:
$ref: "#/definitions/ValueSpecification" - $ref: "#/definitions/ValueSpecification"description: "Public value specifications only."doc:
type: stringdescription: "Optional documentation for the module."description: "Public interface of a module."# ===== Type System =====# All type tags are lowercase in v1Type:
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: arrayminItems: 3maxItems: 3items:
- const: "variable" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name"description: "Represents a type variable (generic parameter)."ReferenceType:
type: arrayminItems: 4maxItems: 4items:
- const: "reference" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName" - type: arrayitems:
$ref: "#/definitions/Type"description: "Type arguments for generic types."description: "Reference to another type or type alias."TupleType:
type: arrayminItems: 3maxItems: 3items:
- const: "tuple" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Type"description: "Element types in order."description: "A composition of multiple types in a fixed order (product type)."RecordType:
type: arrayminItems: 3maxItems: 3items:
- const: "record" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Field"description: "List of field definitions."description: "A composition of named fields with their types."ExtensibleRecordType:
type: arrayminItems: 4maxItems: 4items:
- const: "extensible_record" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name" - type: arrayitems:
$ref: "#/definitions/Field"description: "Known fields."description: "A record type that can be extended with additional fields."FunctionType:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 2maxItems: 2items:
- const: "unit" - $ref: "#/definitions/Attributes"description: "The type with exactly one value (similar to void in some languages)."Field:
type: objectrequired: ["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 v1TypeSpecification:
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: arrayminItems: 3maxItems: 3items:
- const: "type_alias_specification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - $ref: "#/definitions/Type"description: "An alias for another type."OpaqueTypeSpecification:
type: arrayminItems: 2maxItems: 2items:
- const: "opaque_type_specification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters."description: | A type with unknown structure. The implementation is hidden from consumers.CustomTypeSpecification:
type: arrayminItems: 3maxItems: 3items:
- const: "custom_type_specification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - $ref: "#/definitions/Constructors"description: "A tagged union type (sum type)."DerivedTypeSpecification:
type: arrayminItems: 3maxItems: 3items:
- const: "derived_type_specification" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - type: objectrequired: ["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 v1TypeDefinition:
description: "Provides the complete implementation of a type."oneOf:
- $ref: "#/definitions/TypeAliasDefinition" - $ref: "#/definitions/CustomTypeDefinition"TypeAliasDefinition:
type: arrayminItems: 3maxItems: 3items:
- const: "type_alias_definition" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - $ref: "#/definitions/Type"description: "Complete definition of a type alias."CustomTypeDefinition:
type: arrayminItems: 3maxItems: 3items:
- const: "custom_type_definition" - type: arrayitems:
$ref: "#/definitions/Name"description: "Type parameters." - type: arrayminItems: 2maxItems: 2items:
- $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: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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 v1Value:
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: arrayminItems: 3maxItems: 3items:
- const: "literal" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Literal"description: "A literal constant value."ConstructorValue:
type: arrayminItems: 3maxItems: 3items:
- const: "constructor" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName"description: "Reference to a custom type constructor."TupleValue:
type: arrayminItems: 3maxItems: 3items:
- const: "tuple" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Value"description: "Element values in order."description: "A tuple value with multiple elements."ListValue:
type: arrayminItems: 3maxItems: 3items:
- const: "list" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Value"description: "List elements."description: "A list of values."RecordValue:
type: arrayminItems: 3maxItems: 3items:
- const: "record" - $ref: "#/definitions/Attributes" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - $ref: "#/definitions/Value"description: "Dictionary mapping field names to values."description: "A record value with named fields."VariableValue:
type: arrayminItems: 3maxItems: 3items:
- const: "variable" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name"description: "Reference to a variable in scope."ReferenceValue:
type: arrayminItems: 3maxItems: 3items:
- const: "reference" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName"description: "Reference to a defined value (function or constant)."FieldValue:
type: arrayminItems: 4maxItems: 4items:
- const: "field" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Value" - $ref: "#/definitions/Name"description: "Field access on a record. Items: [tag, attributes, recordExpr, fieldName]"FieldFunctionValue:
type: arrayminItems: 3maxItems: 3items:
- const: "field_function" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Name"description: "A function that extracts a field (e.g., .firstName)."ApplyValue:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 4maxItems: 4items:
- const: "lambda" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Pattern" - $ref: "#/definitions/Value"description: | Anonymous function (lambda abstraction).
Items: [tag, attributes, argumentPattern, body]LetDefinitionValue:
type: arrayminItems: 5maxItems: 5items:
- 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: arrayminItems: 4maxItems: 4items:
- const: "let_recursion" - $ref: "#/definitions/Attributes" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Name" - $ref: "#/definitions/ValueDefinition"description: "Multiple bindings that can reference each other." - $ref: "#/definitions/Value"description: "Mutually recursive let bindings."DestructureValue:
type: arrayminItems: 5maxItems: 5items:
- 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: arrayminItems: 5maxItems: 5items:
- 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: arrayminItems: 4maxItems: 4items:
- const: "pattern_match" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Value" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $ref: "#/definitions/Pattern" - $ref: "#/definitions/Value"description: "List of pattern-branch pairs."description: "Pattern matching with multiple cases."UpdateRecordValue:
type: arrayminItems: 4maxItems: 4items:
- const: "update_record" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Value" - type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: arrayminItems: 2maxItems: 2items:
- 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 v1Literal:
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: arrayminItems: 2maxItems: 2items:
- const: "bool_literal" - type: booleandescription: "Boolean literal (true or false)."CharLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "char_literal" - type: stringminLength: 1maxLength: 1description: "Single character literal."StringLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "string_literal" - type: stringdescription: "Text string literal."WholeNumberLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "whole_number_literal" - type: integerdescription: "Integer literal."FloatLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "float_literal" - type: numberdescription: "Floating-point number literal."DecimalLiteral:
type: arrayminItems: 2maxItems: 2items:
- const: "decimal_literal" - type: stringpattern: "^-?[0-9]+(\\.[0-9]+)?$"description: "Arbitrary-precision decimal literal (stored as string)."# ===== Patterns =====# All pattern tags are lowercase with underscores in v1Pattern:
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: arrayminItems: 2maxItems: 2items:
- const: "wildcard_pattern" - $ref: "#/definitions/Attributes"description: "Matches any value without binding (the _ pattern)."AsPattern:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 3maxItems: 3items:
- const: "tuple_pattern" - $ref: "#/definitions/Attributes" - type: arrayitems:
$ref: "#/definitions/Pattern"description: "Patterns for each tuple element."description: "Matches a tuple by matching each element."ConstructorPattern:
type: arrayminItems: 4maxItems: 4items:
- const: "constructor_pattern" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/FQName" - type: arrayitems:
$ref: "#/definitions/Pattern"description: "Patterns for constructor arguments."description: "Matches a specific type constructor and its arguments."EmptyListPattern:
type: arrayminItems: 2maxItems: 2items:
- const: "empty_list_pattern" - $ref: "#/definitions/Attributes"description: "Matches an empty list (the [] pattern)."HeadTailPattern:
type: arrayminItems: 4maxItems: 4items:
- 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: arrayminItems: 3maxItems: 3items:
- const: "literal_pattern" - $ref: "#/definitions/Attributes" - $ref: "#/definitions/Literal"description: "Matches an exact literal value."UnitPattern:
type: arrayminItems: 2maxItems: 2items:
- const: "unit_pattern" - $ref: "#/definitions/Attributes"description: "Matches the unit value."# ===== Value Specifications and Definitions =====ValueSpecification:
type: objectrequired: ["inputs", "output"]
properties:
inputs:
type: arrayitems:
type: arrayminItems: 2maxItems: 2items:
- $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: objectrequired: ["inputTypes", "outputType", "body"]
properties:
inputTypes:
type: arrayitems:
type: arrayminItems: 3maxItems: 3items:
- $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.
In 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.
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 =typeCar={ Plate:string; Seats: int }typeStatus=| 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
Area
Supported
Types
Type abbreviations, records, unions with named or positional fields, generic parameters, tuples, functions, unit, bool, int, int64, float, decimal, char, string, option, Result and list
Values
Literals, 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
Patterns
Wildcard, variable, literal, tuple, constructor, empty list, cons, as and unit, in match and in function and let parameters
Operators and library functions
Arithmetic, 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.
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:
Claim
Value
Claims version
0.1.0-draft.2
Language
fsharp, with the file extension .fs
Several documents in one request
yes (multiDocument: true)
Legacy morphir.frontend.compile
no
Incremental compilation
no
Model tuples
morphir.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:
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.
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.
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.
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 =typeColor=| 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 =typeHandler=delegateof 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 =typeCar={ 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 =typeStatus= 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"#elselet 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 expressionvar intType = new TypeExpr.TInt();
// Create a function typevar 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:
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:
Left
Right
Same version
3
3.0.0
yes
4
4.0.0+build.7
yes: build metadata has no effect
4.1.0-draft.1
4.1.0-draft.1
yes
4.1.0-draft.1
4.1.0
no: a pre-release never matches its release
4.1.0-draft.1
4.1.0-draft.2
no
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:
Builder
Builds
Notes
ClassicBuilder
classic IR, for format versions 1, 2 and 3
Each 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.
V4Builder
IR v4
Each 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
Implement IIrBuilder<…> with the new family’s own node types.
Add a case to FamilyModel for the family’s model.
Implement IFamilyBuilder, and register it in the FamilyBuilderRegistry.
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.
Writer
Declares
Writes
ClassicIrWriter
1, 2, 3
Classic library JSON through ClassicJsonWriter, with the wire profile of the version.
V4IrWriter
4
Canonical 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:
Rule
v1
v2
v3
Type tags
snake_case
PascalCase
PascalCase
Value, pattern and literal tags
snake_case
snake_case
PascalCase
Access wrapper
["public", x]
{"access": "Public", "value": x}
{"access": "Public", "value": x}
Module list
[{"name": path, "def": module}]
[[path, module]]
[[path, module]]
Library tag
library
Library
Library
Module without documentation
doc left out
doc 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:
Outcome
Meaning
Exit code in the CLI
Completed with a passed result
A CompiledPackage: the model in the requested family and the module names. Warnings, information and hints travel with it.
0
Completed with a failed result
Source problems, each a diagnostic with a code and a range.
1
Failed
An 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:stringlist)= Path.fromList ( texts
|> List.map name
)/// One lowering, written once against any family's builder: Main.rate = 60.
let program ={new IBuilderProgram withmember _.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
morphir compile - Compile F# sources to Morphir IR (built into dotnet-morphir)
morphir generate - Generate F# from classic or v4 JSON/Ion libraries
morphir validate - Check a model with the built-in analyzers, such as the type validator
morphir extension worker - Experimental and internal: serve one WASM extension module over stdio for a conformance runner
morphir mck - Run the separately installed Morphir Compatibility Kit, morphir-mck, with the same arguments
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
Planned morphir workspace migrate inventories and adopts projects within a bounded repository directory.
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-detectionmorphir ir verify path/to/morphir-ir.json
# Validate with explicit schema versionmorphir ir verify --schema-version 3 path/to/morphir-ir.json
# Get JSON output for CI/CDmorphir ir verify --json path/to/morphir-ir.json
# Quiet mode (only errors)morphir ir verify --quiet path/to/morphir-ir.json
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 contentmorphir ir verify --schema-version 3 morphir-ir.json
# Test if a v2 IR file is compatible with v3 schemamorphir 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
# Get JSON output for parsingmorphir ir verify --json morphir-ir.json
# Use in scriptsRESULT=$(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 failsmorphir ir verify --quiet morphir-ir.json
# Use in CI/CDif morphir ir verify --quiet morphir-ir.json; then echo "IR is valid"else echo "IR validation failed" exit 1fi
Exit Codes
Code
Meaning
Description
0
Success
IR file is valid according to the schema
1
Validation failure
IR file failed schema validation (see error output)
2
Operational error
File 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 IRrun: | 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 1fidoneecho "All IR files are valid!"
JSON Output Parsing
Parse JSON output with jq:
# Check if validmorphir ir verify --json morphir-ir.json | jq '.IsValid'# Get error countmorphir ir verify --json morphir-ir.json | jq '.Errors | length'# Extract error messagesmorphir ir verify --json morphir-ir.json | jq '.Errors[].Message'# Get schema version usedmorphir 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:
Check the formatVersion field in your JSON (for v2/v3)
Use --schema-version explicitly to override auto-detection
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:
Check schema version - ensure you’re validating against the correct version
Review error messages - they include expected vs. found values
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 platformmorphir dist list
# List distributions for a specific platformmorphir dist list --platform linux-x64
# List project-local distributionsmorphir 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 globallymorphir dist install https://example.com/morphir-dist.tar.gz 1.0.0
# Install locally for projectmorphir dist install https://example.com/morphir-dist.tar.gz 1.0.0 --local
# Install for specific platformmorphir 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 versionmorphir dist use 1.0.0
# Set local active version for current projectmorphir 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)
--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 yetmorphir extension remove <name> <version> [--platform <rid>][--local]# not implemented yetmorphir 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.
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:
Offering
host
deployment
execution
platform
Top-level prerequisites
morphir-dotnet.fsharp
Native AOT morphir
morphir
native-aot
native-aot
the RID
none
not available, with the reason above
Self-contained morphir
morphir
self-contained
jit
the RID
none
available
Framework-dependent morphir
morphir
framework-dependent
jit
the RID
runtime Microsoft.NETCore.App>=10.0.0<11.0.0
available
dotnet-morphir (Morphir.Tool)
dotnet-morphir
dotnet-tool
jit
left out: the package is portable
that 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.
Field
Meaning
host
The 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.
deployment
The 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.
execution
How this process runs: native-aot or jit.
platform
The .NET runtime identifier that the profile built for. Left out for the portable tool package and when no named profile built the executable.
prerequisites
What the offering needs from the machine: runtime (an installed .NET runtime, to run), sdk (a .NET SDK, with phaseinstall or run) and native (none is declared yet). Left out when no named profile built the executable.
deployments
The offerings in which the extension is available.
reason
Why the extension is not available in the running offering. Present exactly when available is false.
extension prerequisites
What the extension needs from the machine beyond the host. Every built-in needs nothing.
id
The extension id. See Extension ids. For a built-in generator, it is also the backend value in morphir generate --json reports.
version
Canonical SemVer 2.0.0.
description
Left out when the extension has none.
types
MEP extension types: frontend, backend, transform, validator or workspace. They come from the capabilities.
delivery
How 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.
hosts
The hosts that the extension runs in.
available
true 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.frontend
The MEP frontend claim: the languages with their file extensions, and the IR versions that it produces.
capabilities.backend
The MEP backend claim: targets, IR versions in their declared spelling, and generate.
capabilities.validator
The 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:
Platform
RID
Linux x64
linux-x64
Linux ARM64
linux-arm64
Windows x64
win-x64
Windows ARM64
win-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:
Project-local selection (if .morphir/ exists and selection is set locally)
Global selection (in OS config directory)
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):
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]
Command
Result
get <key>
One typed value; a missing key is distinct from an empty string or array
list
Ordered canonical key/value entries; arrays are complete values
show
The nested document
path
Consulted 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.
Selector
Sources
--workspace
Workspace primary files
--project <name-or-path>
Primary files of one selected project; a relative path starts at the workspace root
--user
Personal overrides adjacent to the selected project or root
--global
Global user files
--system
System 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
Option
Purpose
--show-origin
Include winning paths, formats and available positions with entries
--show-scope
Include winning source roles with entries
--json
Emit 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-profiles
Disable 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
--help
Show 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:
Variable
Mirrors
Accepted values
MORPHIR_DISCOVERY_BOUNDARY
--discovery-boundary
A directory. A relative path resolves against the current directory. Native Windows drive and UNC paths work with either separator
MORPHIR_ISOLATED
--isolated
true, 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.
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.
Field
Type and meaning
operation
get, list, show, path or explain
scope
effective, workspace, project, user, global, system or file:<canonical-path>
key
Canonical quoted key path, or null
found
Boolean; distinguishes missing keys from present empty values
value
JSON scalar, array or object; null for a missing key or an operation without a value
entries
Array of {key, value, origins}
sources
Array of {source, status, contributed, detail}
contributions
Array of {source, value, winner, origins}
diagnostics
Array of {code, severity, message, path, key, line, column}
valueTypes
Array of {pointer, kind, encoding} for canonical types requiring JSON metadata
exitCode
Integer 0, 1 or 2
containment
path 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:
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:
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
Code
Meaning
0
Successful inspection, preview or edit, including unchanged and overridden writes
1
Requested get/explain key or targeted unset entry not found
2
Usage, 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:
--type
Input
string
Literal command argument, including an empty string
bool or boolean
true or false
int or integer
Signed 64-bit JSON integer
number
Finite JSON number, retained as a TOML float
array
Complete JSON array
object
Complete 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.
Field
Meaning
operation
set or unset
outcome
preview, written, unchanged, conflict, rolled-back, recovery-required, io-failure or failed
target, key
Selected source metadata and canonical quoted path, or null before target selection
before, after
Redacted target-entry values; null represents an absent entry
changed, dryRun
Whether the proposed target differs, and whether this is a preview
effective
The nested inspection explanation with contributions, origins and its own type metadata
diagnostics
Safe codes and source/key/position metadata
recoveryArtifacts
Retained {path, kind, transactionId} records
exitCode
Stable outer command category, 0/1/2
valueTypes
Type 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.
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.
<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.
TOML target in a supported primary layout at the same project location; defaults to adjacent morphir.toml
--dry-run
Return mappings, edits, conflicts, and verification without writing
--json
Emit 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.
Code
Meaning
0
Verified success, applicable preview, or unchanged repeat
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.
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
Option
Meaning
--output path
Choose a supported TOML primary layout at the same project location
--dry-run
Return the complete plan without creating or changing files
--json
Emit one structured JSON result on stdout
--discovery-boundary directory
Bound 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.
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
Exit
Meaning
0
Verified application, applicable preview, or unchanged repeat
2
Invalid 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.
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.
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.
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-run
Preview the complete workspace and project plan without writing
--json
Emit 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.
Code
Meaning
0
Verified success, applicable preview, or unchanged existing workspace
2
Invalid or blocked plan, failed verification/application, or recovery required
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.
<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
Control
Meaning
<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-run
Return the complete verified plan and write nothing
--json
Write 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
--help
Show 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.
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.
Planned changes, preview mode, and aggregate verification
inventory
Path, relative path, kind, reason, and project name
projects
Per-project redacted ledger, edits, conflicts, and independent verification
members, workspaceTarget, workspaceEdits
Reviewed membership and workspace primary edits
adoptionEffects
Intentional ownership, membership, default-selection, and output-root effects
diagnostics
Structured warnings and blockers also sent to stderr
recoveryArtifacts, restoredPaths, unresolvedPaths
Retained owned evidence and recovery state
uncertainDirectories, preservedDirectories
Directory paths whose ownership is uncertain, or that explicit recovery was instructed to preserve
valueTypes
Typed 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.
Applicable preview, successful application, or valid existing-workspace no-op
2
Invalid 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:
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.
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.
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
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.
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
Code
Meaning
The tool’s code
morphir-mck ran.
126
The resolved file could not be started.
127
No tool to run: none found, or MORPHIR_MCK was refused.
143
Linux 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.
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.
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
The command that wrote the report: morphir or dotnet-morphir. The Native AOT morphir and the managed morphir both report morphir.
platform
The 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.
The 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[].profiles
Only when available: the WASM execution profiles the host runs. This host runs extism-kernel-v1 only.
runtimes[].bounds
Only 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
Reason
What it means
What you can do
platform_unsupported
The 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_missing
The 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_failed
The 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_unsupported
WASM 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.
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.
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:
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
Option
Meaning
--kit
The suite directory, for example conformance/suites/wasm-host. The pinned corpus is found in the checkout that holds it.
--host
The host executable.
--host-arg
An argument before extension worker, repeatable. For example --host dotnet --host-arg exec --host-arg worker.dll.
--host-profile
A 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, --engine
Required. 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.
--report
The report file to write.
--deviations
A 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 digests
A passing generic guest kit run through the same executable, cited by the report. The cited host digest must be the --host digest.
Outcomes
Outcome
Meaning
passed
Every 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.
failed
A 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.
error
The runner could not judge the case, for example because no started event declared a bound the case depends on. Never a pass.
not_applicable
The case needs a profile the host does not declare.
not_enforced
The host’s started events declare a bound of the case not enforced.
refused
The module’s bytes differ from the recorded digest; no worker started.
Exit codes
Code
run
report-check
0
Every 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.
1
A 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.
2
An 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.
The report (morphir/mck-wasm-host-report0.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
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.
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.
See what is installed:
morphirup show
"status": "shown" means a toolchain is set up and selected.
Switch to another installed toolchain when you need to:
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.
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>.
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.
Releases of morphir-dotnet attach morphirup for five platforms, beside the morphir assets on the
releases page:
Platform
Executable
Archive
Linux glibc x64
morphirup-linux-x64
morphirup-linux-x64-v<version>.tar.gz
Linux glibc ARM64
morphirup-linux-arm64
morphirup-linux-arm64-v<version>.tar.gz
Windows x64
morphirup-win-x64.exe
morphirup-win-x64-v<version>.tar.gz
Windows ARM64
morphirup-win-arm64.exe
morphirup-win-arm64-v<version>.tar.gz
macOS ARM64
morphirup-osx-arm64
morphirup-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 wantbase=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"
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.
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 code
Meaning
0
The result is planned or succeeded
1
The result is failed: a document was refused or a check failed
2
morphirup did not run the command; see Exit code 2. Nothing is written to standard output; the reason is on standard error
3
Provisional. 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:
File
Largest size read
Request (.morphir/toolchain.toml or --request)
1 MiB
Lock (.morphir/toolchain.lock.json or --lock)
1 MiB
--artifact-sizes
1 MiB
--recipe
16 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.
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.
Option
Meaning
--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 --version
The 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.
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 documentlock 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 (schemaVersion1);
the extension release descriptor a provider binds (schemaVersion2.0.0-draft.2);
the capability claim set of each of its artifacts (claimsVersion0.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 causecrlf-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.
src/Morphir.Toolchain, the document readers, resolution and planning this command uses
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.
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
Option
Default
Meaning
--abi <profile>
required
The 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>
required
The module file. A relative path is resolved against the current directory.
--build-identity <hex>
required
This 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>
5000000000
The 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.
--json
off
Write the report as morphir/command-event lines.
--help, -h
Write 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.
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: ..."}}
Event
Members after the envelope
When
started
abi, moduleSha256, buildIdentity, bounds
The module has loaded. bounds has exactly memoryCeiling, instructionBudget and callDeadline, each {"enforced": true}: whether the bound is enforced, not its value.
output
output; or output and droppedBytes
Guest 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.
fault
fault.kind, fault.message
The 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.
refused
error.code, error.message
A 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.
exited
exitCode
Last, 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.codeusage); 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.
Use exact target csharp, numeric language-version=14 or 15, and an optional
literal C# namespace. These invocations fail with exit 2 before publication:
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:
Check if tools are in PATH:
echo $PATH | grep .dotnet/tools
Add to PATH if missing (Linux/macOS):
export PATH="$PATH:$HOME/.dotnet/tools"
Add to PATH if missing (Windows):
$env:PATH += ";$env:USERPROFILE\.dotnet\tools"
Restart your terminal after modifying PATH
Installation Fails with “Unable to find package”
Problem:dotnet tool install Morphir.CLI fails to find the package.
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
Compare with known-good IR:
# Validate a reference filemorphir ir verify reference-morphir-ir.json
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
Problem: Running morphir dist which or morphir tool which <name> shows “No active version set”.
Solution:
List installed versions:
morphir dist list
morphir tool list
Set an active version:
# Globalmorphir 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.
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 installationsmorphir 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:
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.
Code
Cause
Fix
compile.frontend_unavailable
No 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.unavailable
The 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_admitted
The 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_unavailable
The 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.unrepresentable
The 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_module
project.exposed_modules names a module that the sources do not define.
Fix the module name, or remove it from exposed_modules.
The source directory has no file with an extension that the frontend claims.
Set project.source_directory, or use --input.
sources.unsupported_entry, sources.invalid_utf8
A 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_missing
generate 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.
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.
Source language, for example fsharp. Overrides frontend.language.
--input, -i
Source files in compilation order, or exactly one directory.
--project-file
One build-tool project file that names the ordered sources. See project-file sources.
--package-name
Package name, for example acme/car-rental. Overrides project.name.
--ir-version
IR format version: 1, 2, 3, 4, or a SemVer version such as 4.0.0. Overrides ir.format_version.
--project
A project directory, a workspace member path, or a project name.
--config
An explicit configuration file.
--extension
A frontend extension: a command name, a path, or a .dll. Overrides [frontend.<language>].extension.
--output, -o
Install a copy of the IR in this directory after a successful compile. See Output.
--out-dir
The out root. Overrides MORPHIR_OUT_DIR and workspace.out_dir.
--target
A generation target, for example fsharp or csharp. Generate code from the compiled IR. See Compile and generate.
--generate-output
The owned output directory of the generation. Necessary with --target.
--option
A backend option KEY=VALUE for --target, as in generate. You can give it more than once.
--json
Write one JSON report to stdout.
--json-lines
Write the compile events to stdout as JSON Lines, and end with the report.
--types-only
Not 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:
a command-line option;
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;
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:
--project-file: one project file;
--input: files in the given order, or one directory;
[frontend.<language>].project_file of the selected language;
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:
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:
Code
Meaning
extension.unavailable
The 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_admitted
The agreed session does not admit the request, for example its IR version.
extension.tuple_unavailable
The host and the guest agreed no compileModel tuple, so no request can be sent.
extension.protocol_error
The guest broke the protocol, for example it wrote a log line to stdout.
extension.timeout
The 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:
--out-dir;
MORPHIR_OUT_DIR, relative to the invocation directory;
the workspace workspace.out_dir, relative to the workspace root;
.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:
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.
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
Code
Meaning
0
Success. Warnings do not change the exit code.
1
The frontend, an IR builder or the writer reported a source or model error.
2
An invocation, configuration, project-file, selection, extension or I/O failure.
130
The 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.
The language and the selected frontend, when known.
irVersion
The requested IR version, exactly as written.
sources
The 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.
modules
The compiled modules, in compilation order.
output
The published IR path, or null.
installed
The absolute files that the -o install of this run holds. Empty without -o.
diagnostics
Every diagnostic of the run, ordered by stage, then document, then position.
failureKind
source, invocation, configuration, operational or cancelled, or null.
origins
Where each setting came from: cli with its flag, configuration with its file and key, or default.
generation
Only 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:
$ 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.
event
Other members
Meaning
stageStarted
stage
A stage starts.
diagnostics
stage, optional document, diagnostics
The diagnostics of a stage for one document, or for no document.
stageCompleted
stage, passed
A stage completes.
completed
report
The 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.
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".
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.
JSON/Ion model file or Ion tree directory. Without it, the IR of the most recent compile
--target
Required exact registered target ID, fsharp or csharp
--output
Required output directory with owned generated files
--input-format
auto by default; json or ion constrains the reader
--option
Repeatable backend option, described below
--json
Write one structured report to stdout
--extension
A backend extension: a command name, a path, or a .dll. Overrides [codegen.<target>].extension. See Backend extensions.
--project
The project directory, workspace member path or project name: its compile record without --input, and its configuration
--config
An explicit configuration file of the project
--out-dir
Without --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
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:
Code
Meaning
extension.unavailable
The 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_unavailable
The 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_admitted
The 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_artifact
The 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_error
The 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.timeout
The guest did not answer before the deadline.
invalid-extension
The 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:
--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:
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 (backendTransportmep) 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.
Exit
Meaning
0
Generation and publication succeeded
1
Model or generation semantics are unsupported or invalid
2
Invalid invocation, unavailable provider or operational failure
130
Cancelled 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.
Required. A JSON or Ion model file, or an Ion tree directory. The path resolves against the invocation directory.
--input-format
auto (the default), json or ion. json and ion select the reader.
--json
Write 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.
Id
Reads
Reports
morphir.type-validator
Classic models, format versions 1, 2 and 3
Definite 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:
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.
The host that wrote the report: morphir or dotnet-morphir. The two hosts write the same report apart from this field.
success
true when the exit code is 0.
exitCode
The exit code of the command.
input
The full path of the input.
family, formatVersion
The model’s family (Classic or V4) and its declared format version. Left out when the input could not be read.
analyzers
Each analyzer, ordered by id, with its status: ran, failed (with a detail) or not-run (with a reason: turned-off or unsupported-model).
diagnostics
The 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
Code
Meaning
0
No diagnostic has the severity error. Warnings and information are in the result.
1
At least one diagnostic has the severity error.
2
An invalid invocation, an input or configuration that cannot be read, or an analyzer that failed.
130
The 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:
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.
'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 thenlet 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 plugin
Later plugins
Compilers
Adds an Info or Warning message
Run
Run
Adds an Error message and returns the model
Run
Do not run
Adds a Fatal message
Do not run
Do not run
Returns None
Do not run
Do 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:stringlist) file -> Some rows, file |> VFile.set rowCount rows.Length)let reportRows = Plugin.create "report"(fun(rows:stringlist) 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:
typeModel={ Version: int; Rows:stringlist}let sortRows = Plugin.create "sort"(fun(rows:stringlist) 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:
Options
Rewrites
Optimizer.foldingOnly
Folds SDK operations on literals. Never changes which models a later stage accepts.
Optimizer.all
Also 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.
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:
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;
varvalue = 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.
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:
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:
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.
Check
Diagnostic
What to do
A file exists at path
The WASM module of extension 'python' cannot be loaded: there is no file at '<path>'.
Correct path.
The file can be read
The 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 module
The 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 features
extension.wasm_feature_unsupported
The 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 it
extension.abi_unsupported
The 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 profile
extension.abi_mismatch
The 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.
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.
The 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.
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:
Review Capability: Every guru includes proactive review capability
Cross-Agent Design: Ensure portability across Claude, Copilot, Cursor, and other agents
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:
A clear definition of what makes a guru
Repeatable patterns for creation
Cross-agent accessibility (not Claude-only)
Continuous improvement mechanisms
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
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
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
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
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:
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:
Support existing tools: Maintain compatibility with morphir-elm ecosystem
Enable migration: Allow existing morphir-elm code to migrate to F# (and eventually other languages)
Reserve evolution space: Keep Morphir.IR namespace free for future improvements
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.
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:
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):
Version Management - Semantic versioning, version detection
Changelog Management - Keep a Changelog format, parsing, generation
Deployment Orchestration - Workflow automation, status tracking
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
---
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]
#!/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:
Check GitHub Actions status (manual every 5 min = many tokens)
Validate post-release status (manual testing = tokens)
Extract release history for notes (manual searching = tokens)
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:
Input: What data does this need?
Processing: What analysis/transformation?
Output: What does it return?
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)
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:
Collect: Gather all feedback from past quarter
Analyze: Identify 2-3 key improvements
Decide: What will change? What won’t?
Update: Modify playbooks, templates, patterns
Document: Record what changed and why
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.
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)
## 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”)
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)
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.
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
Question
Answer
Justification
1. Is it a distinct domain?
YES
Technical 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?
YES
30+ 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?
YES
9 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?
YES
Link 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?
YES
Release 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)
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
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
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
Visual Communication & Diagramming
Mermaid Mastery: Flowcharts, sequence diagrams, class diagrams, state diagrams, entity relationship diagrams, Gantt charts, pie charts, journey maps
---
name: technical-writerdescription: "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)
1. link-validator.fsx
Purpose: Validate internal and external documentation links
The Technical Writer skill should proactively look for:
Broken Links: Internal and external links that no longer work
Stale Examples: Code examples that don’t compile or produce wrong output
Missing Documentation: Public APIs without documentation
Style Violations: Documentation not following style guide
Outdated Content: Documentation that doesn’t match current behavior
Orphaned Content: Documentation that’s no longer referenced
Accessibility Issues: Content that isn’t accessible
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 Type
When
Output
CI/CD Push
Documentation file changed
Link validation report
PR Review
PR includes documentation
Doc quality checklist
Weekly Schedule
Sunday midnight
Style compliance report
Quarterly Review
First week of quarter
Comprehensive audit
Manual Request
User invokes review
Full documentation report
Release Preparation
Before release
Release 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:
Understand the feature
Read PR description and linked issues
Review code changes
Identify public APIs
Note configuration options
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
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
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
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
Validate documentation
Run link validator
Test all code examples
Review for style compliance
Get peer review
Output: Complete documentation package for the feature
Don’t modify Docsy files directly (they’re in go modules)
For layout changes
Copy Docsy template to layouts/ with same path
Modify the copy (original stays in module)
Test thoroughly - Docsy updates may conflict
For navigation changes
Configure in hugo.toml under [menu]
Use weight in frontmatter for ordering
Use _index.md files for section pages
For new shortcodes
Create in layouts/shortcodes/
Name file shortcodename.html
Reference in content using angle-bracket shortcode syntax
Test changes
hugo server -D # Include drafts# Check at http://localhost:1313
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>/// <paramname="paramName">Description of parameter including valid values.</param>/// <returns>Description of return value, including null/empty cases.</returns>/// <exceptioncref="ArgumentException">When paramName is invalid because...</exception>/// <example>/// <code>/// var result = MyMethod("value");
/// // Use result for...
/// </code>/// </example>/// <seealsocref="RelatedClass"/>/// <seealsohref="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
### 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.0Description of what this option does andwhentouse 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: 10date: 2025-01-15lastmod: 2025-01-20draft: falsetoc: truecategories:
- Guidestags:
- getting-started - tutorial---
Field Guidelines:
Field
Purpose
Best Practice
title
SEO, browser tab
Descriptive, include keywords
linkTitle
Navigation menu
Short (2-4 words)
description
Search/social preview
Single sentence, < 160 chars
weight
Menu ordering
Lower = higher in menu
date
Creation date
ISO 8601 format
lastmod
Last modification
Auto if enableGitInfo=true
draft
Hide from build
Set false when ready
toc
Table of contents
true 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
Start with the big picture
High-level architecture diagram
3-5 main components
No implementation details
Then zoom into details
Detailed view of each component
Show interfaces and interactions
Include relevant code snippets
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
Starting Work: Check the status to see what’s being worked on
Implementation: Update the PRD’s Feature Status Tracking table as you complete features
Design Decisions: Add Implementation Notes to capture important decisions
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?”:
Check this index for active PRDs
Open the relevant PRD and find the Feature Status Tracking table
Look for features with status ⏳ Planned (ready to start) or 🚧 In Progress
Update feature status in real-time as work progresses
Add Implementation Notes for significant design decisions
Creating a New PRD
Copy an existing PRD as a template
Fill in all sections with comprehensive detail
Include Feature Status Tracking table with all planned features
Add to this index with “Draft” status
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:
Validate IR correctness: Verify that generated or hand-written IR files conform to the expected schema
Debug format issues: Quickly identify structural problems in IR files
Ensure version compatibility: Confirm which schema version an IR file uses and whether it’s valid
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
Enable IR validation via CLI command for all supported schema versions (v1, v2, v3)
Establish WolverineFx integration with Vertical Slice Architecture as the foundation for future tooling commands
Provide excellent developer experience with clear, actionable error messages and multiple output formats
Support flexible input starting with file paths, with extensibility for stdin and multiple files
Auto-detect schema versions while allowing manual override when needed
Secondary Goals
Create reusable validation services in Morphir.Tooling that can be leveraged by other tools
Establish testing patterns using BDD scenarios for validation use cases
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):
Look for formatVersion field in JSON
Analyze tag capitalization patterns:
All lowercase tags → v1
Mixed capitalization → v2
All capitalized tags → v3
If ambiguous, report detection failure with suggestions
Manual Override:
--schema-version option forces validation against specified version
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
// Add new IR subcommandvar 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 busvar 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);
# Features/VerifyMultipleIR.featureScenario: 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.featureScenario: 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
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/.
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 0And 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 0And 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 0And 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 0And 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 1And 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 1And 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 1And the output should contain "✗ Validation failed"
And the error count should be greater than 0Scenario: 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 1And 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 v2And 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 0And 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 1And 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 0Scenario: 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 0And 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 1And 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 0And 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 1And 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 12When I run "morphir ir verify line-col-error.json"
Then the exit code should be 1And 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 1And 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 2And 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 2And 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 2And 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 0Scenario: 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 2And 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 2And 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": 3When I run "morphir ir verify with-format-v3.json"
Then the validation should use schema v3And 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 v1And 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 v3And 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 v2And 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 0And 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": 3When 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 0And 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 1And 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 1And 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 0And 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 1And 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 0And 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 0And 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 1And 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 0And 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 1And 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 12When 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 42When I run "morphir ir verify --verbose error.json"
Then the output should include a code snippet around line 42And 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 0Examples:
| 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/:
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:
Fragmented Knowledge: Morphir spans multiple repositories (morphir-elm, morphir-jvm, morphir-scala, morphir-dotnet, etc.) with varying maturity levels, features, and conventions
Inconsistent Issue Quality: Issues and PRs often lack context, proper categorization, or alignment with project goals
PRD Gaps: Not all features have comprehensive PRDs, and creating high-quality PRDs requires deep Morphir knowledge
Cross-Repo Blind Spots: Contributors may duplicate work or miss opportunities for cross-repository synergies
UX/DX Debt: User experience and developer experience improvements need dedicated advocacy
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
Expert PRD Guidance: Help users create comprehensive, well-structured PRDs aligned with Morphir principles
Issue Quality Improvement: Assist in crafting high-quality issues (bugs, features, enhancements) with proper context
Ecosystem Intelligence: Provide real-time awareness of backlogs, trends, and status across all Morphir repositories
UX/DX Advocacy: Champion user and developer experience improvements
Intelligent Questioning: Push back constructively on features that don’t align with Morphir’s ethos
GitHub Automation: Provide F# scripts for querying, analyzing, and reporting across the ecosystem
Secondary Goals
Cross-Skill Integration: Coordinate effectively with qa-tester and release-manager skills
Knowledge Management: Maintain and share institutional knowledge about Morphir
Template Library: Provide reusable templates for common product management tasks
Metrics & Analytics: Track and report ecosystem health metrics
Non-Goals
Explicitly Out of Scope
Code Implementation: Development agents handle implementation
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
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:
Core Specification (finos/morphir)
Language specification
IR schema definitions (v1, v2, v3)
Authoritative documentation
Reference Implementation (finos/morphir-elm)
Elm frontend compiler
CLI tools
Example models
Most mature implementation
Platform Implementations:
finos/morphir-jvm: Java/Kotlin support
finos/morphir-scala: Scala support
finos/morphir-dotnet: C#/F# support
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)
queryEcosystemIssues {
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.
// 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
Q3: Should F# scripts use GitHub CLI or direct API calls?
Status: Open
Options:
GitHub CLI (gh) for simplicity and auth
Direct API calls via HTTP client for flexibility
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:
Label frequency and time-series only
Add NLP for theme extraction from titles/descriptions
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
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.
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)
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
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():
publicstatic SemVersion GetVersionFromChangelog(AbsolutePath changelogPath)
{
var content = File.ReadAllText(changelogPath);
var parser = new ChangelogParser();
var result = parser.Parse(content);
if (!result.IsSuccess)
thrownew Exception($"Failed to parse CHANGELOG.md: {result.Error}");
var changelog = result.Value;
var latest = changelog.SectionCollection.FirstOrDefault()
?? thrownew Exception("No releases found in CHANGELOG.md");
if (!SemVersion.TryParse(latest.MarkdownVersion, SemVersionStyles.Any, outvar version))
thrownew Exception($"Invalid version: {latest.MarkdownVersion}");
return version;
}
2.3 Implement Release Notes Extraction
Create ChangelogHelper.GetReleaseNotes():
publicstaticstring GetReleaseNotes(AbsolutePath changelogPath)
{
var content = File.ReadAllText(changelogPath);
var parser = new ChangelogParser();
var result = parser.Parse(content);
if (!result.IsSuccess) returnstring.Empty;
var latest = result.Value.SectionCollection.FirstOrDefault();
if (latest == null) returnstring.Empty;
var notes = new StringBuilder();
AppendSection("Added", latest.SubSections.Added);
AppendSection("Changed", latest.SubSections.Changed);
// ... other sectionsreturn notes.ToString();
}
publicstatic SemVersion GetNextPreReleaseVersion(AbsolutePath changelogPath)
{
var currentVersion = GetVersionFromChangelog(changelogPath);
if (!currentVersion.IsPrerelease)
thrownew Exception("Cannot auto-bump non-prerelease version");
// Extract pre-release type and number// e.g., "alpha.1" → type: "alpha", number: 1var prereleaseParts = currentVersion.Prerelease.Split('.');
var type = prereleaseParts[0]; // alpha, beta, preview, rcvar number = int.Parse(prereleaseParts.Length > 1 ? prereleaseParts[1] : "0");
// Increment number number++;
// Create new versionvar newPrerelease = $"{type}.{number}";
returnnew 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-20When I call GetVersionFromChangelog()
Then version should be 0.2.1Scenario: 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-20And [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"
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: Deploymenton:
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: falsejobs:
validate-version:
runs-on: ubuntu-latestoutputs:
version: ${{ steps.get-version.outputs.version }}steps:
- uses: actions/checkout@v4 - name: Get version from CHANGELOGid: get-versionrun: | # 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 CHANGELOGrun: | VERSION=${{ steps.get-version.outputs.version }}
if ! grep -q "\[$VERSION\]" CHANGELOG.md; then
echo "Version $VERSION not found in CHANGELOG.md"
exit 1
fibuild-executables:
needs: validate-version# ... existing build-executables jobs ...release:
needs: [validate-version, build-executables]runs-on: ubuntu-lateststeps:
- uses: actions/checkout@v4 - name: Setup .NET SDKuses: actions/setup-dotnet@v4with:
global-json-file: global.json - name: Restore dependenciesrun: ./build.sh Restore - name: Buildrun: ./build.sh Compile - name: Run testsrun: ./build.sh TestAll # Includes build tests! - name: Download executablesuses: actions/download-artifact@v4 - name: Pack packagesrun: ./build.sh PackAll - name: Run build testsrun: ./build.sh TestBuild - name: Publish to NuGetrun: ./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-lateststeps:
- uses: actions/checkout@v4 - name: Download executablesuses: actions/download-artifact@v4with:
path: artifacts/executables - name: Extract release notes from CHANGELOGid: release-notesrun: | 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 Releaseuses: softprops/action-gh-release@v1with:
tag_name: v${{ needs.validate-version.outputs.version }}name: Release v${{ needs.validate-version.outputs.version }}body_path: release-notes.mdfiles: | artifacts/executables/morphir-*
artifacts/executables/morphir.exeenv:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Target PublishTool => _ => _
.DependsOn(PackTool)
.Description("Publish Morphir.Tool to NuGet.org")
.Executes(() =>
{
if (string.IsNullOrEmpty(ApiKey))
thrownew Exception("API_KEY required");
var toolPackage = OutputDir.GlobFiles("Morphir.Tool.*.nupkg")
.FirstOrDefault();
if (toolPackage == null)
thrownew 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:
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
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-20And all changes are committed
When I create and push tag v0.2.1Then 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.1And morphir-win-x64 should be in GitHub release v0.2.1And morphir-osx-arm64 should be in GitHub release v0.2.1And 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.1When I build all packages
Then all packages should have version 0.2.1And 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
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.
The guide describes target rules. The CLI reference records implemented inspection and editing interfaces.
Goals
Share core functionality across hosts without a dependency on the CLI.
Match MoonBit discovery, precedence, legacy manifest normalization, and project selection.
Preserve typed values and winning origins.
Integrate with Microsoft.Extensions.Configuration through an adapter.
Support scoped inspection and validated TOML editing.
Document current support separately from planned behavior.
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.
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.
Order
OpenSpec change
Outcome
Prerequisites
1
add-shared-configuration
Typed documents, format readers, resolution, profiles, provenance, and validation shared by hosts
None
2
add-project-workspace-discovery
Bounded roots, member discovery/selection, and applicable sources
1
3
add-config-inspection
Shared queries, read-only config commands, and opt-in Microsoft configuration adapter
1, 2
4
add-config-editing
Shared TOML edit planning/application and config set/unset
3
5
add-classic-project-migration
Shared loss-checked migration and project migrate
2, 4
6
add-workspace-migration
Bounded repository adoption and coordinated workspace migrate
5
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:
Its implementation, tests, and docs are in a reviewable PR; required checks and reviews pass on the delivered head.
Its OpenSpec changes are archived and the main specs are synchronized coherently with the delivered implementation.
The PR is merged to main, and authoritative merge evidence identifies a merged commit reachable from the updated main branch.
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.
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.
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.
Campaigns 3 and 4, PR #475; shared plans, TOML editor, transactions and parent cleanup confirmed
Classic project migration
Implemented and landed
Campaign 5, #476; pure planner, independent verification, guarded IO, project migrate and managed/trimmed/native qualification
Workspace migration
In Progress
Bounded inventory, coordinated project plans, membership, and recovery
Config CLI commands
Implemented and landed
Campaign 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 foundation
Updated for inspection and editing
Shared 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 verification
In Progress
Campaign 1 landed baseline tooling 245, reader 66, core 95 and warning-qualified runtime evidence; current campaign results recorded separately
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.
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.
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:
Developers must manually research each CVE to determine if it’s genuine or a false positive
There’s no standardized process for documenting suppression decisions
Suppression files must be manually created following OWASP Dependency-Check XML schema
No easy way to trigger scans on specific branches during development
No guided workflow for fix vs. suppress decisions
Success Criteria
Automation: Reduce manual effort for vulnerability resolution by 70%
Documentation: 100% of suppressions have documented rationale
Auditability: Clear audit trail for all security decisions
Developer Experience: Interactive prompts guide users through resolution
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:
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: falsedefault: 'main'fail-cvss:
description: 'Fail on CVSS score >= N'required: falsedefault: '7'suppression-file:
description: 'Path to suppression file'required: falsedefault: './dependency-check-suppressions.xml'
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
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.
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.
Runner
Passed
Morphir.CodeGeneration.FSharp.Tests
333
Morphir.Configuration.IO.Tests
266
Morphir.Configuration.Inspection.Smoke
71
Morphir.Configuration.Microsoft.Tests
22
Morphir.Configuration.Tests
149
Morphir.Core.Tests
237
Morphir.IR.Pipeline.Plugins.Tests
141
Morphir.IR.Pipeline.Tests
299
Morphir.Internal.CodeGeneration.Tests
25
Morphir.Ion.Tests
463
Morphir.Models.Tests
791
Morphir.Myriad.Integration.Tests
12
Morphir.Myriad.Tests
4
Morphir.SDK.CSharp.Tests
20
Morphir.SDK.Conformance.Tests
27
Morphir.SDK.Tests
403
Morphir.Tooling.Tests
360
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.
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.
Behavior
Failing evidence
Passing evidence
Replacement rollback
red-rollback.log
green-rollback.log
Changed source or absent alternate primary
red-recheck.log
green-recheck.log
Linked ancestor rejection
red-links.log, red-target-guard.log
green-links.log, green-target-guard.log
Directory evidence with owned staging
red-directory.log
green-directory.log
Overlapping writers
red-lock.log
green-lock.log, cross-process.log
Incomplete recovery journal
red-journal.log
green-journal.log
Cleanup failure
red-cleanup.log
green-cleanup.log
Recovery target and backup integrity
red-recovery-guard.log
green-recovery-guard.log
Partially created stage ownership
red-partial-stage-build.log
green-partial-stage.log
Proven post-rollback outcome
red-fault-matrix.log
green-fault-matrix.log
Existing Unix permissions
red-mode.log
green-mode.log
Target evidence ownership
red-plan-guard.log
green-plan-guard.log
Stage and backup integrity before installation
red-stage-integrity.log
green-stage-integrity.log
Unambiguous inventory identities
red-inventory.log
green-inventory.log
Partially created parent ownership
red-parent-ownership.log
green-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.
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.
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:
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.
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.
Runner
Passed
Morphir.CodeGeneration.FSharp.Tests
333
Morphir.Configuration.IO.Tests
307
Morphir.Configuration.Inspection.Smoke
71 checks
Morphir.Configuration.Microsoft.Tests
22
Morphir.Configuration.Tests
175
Morphir.Core.Tests
237
Morphir.IR.Pipeline.Plugins.Tests
141
Morphir.IR.Pipeline.Tests
299
Morphir.Internal.CodeGeneration.Tests
25
Morphir.Ion.Tests
463
Morphir.Models.Tests
791
Morphir.Myriad.Integration.Tests
12
Morphir.Myriad.Tests
4
Morphir.SDK.CSharp.Tests
20
Morphir.SDK.Conformance.Tests
27
Morphir.SDK.Tests
403
Morphir.Tooling.Tests
397
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 1dotnet 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.
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.
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.
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.
Project
Runner flags
Passed
Morphir.CodeGeneration.FSharp.Tests
--sequenced
333
Morphir.Configuration.IO.Tests
--maximum-parallel-tests 1
429
Morphir.Configuration.Inspection.Smoke
none
71
Morphir.Configuration.Microsoft.Tests
--maximum-parallel-tests 1
22
Morphir.Configuration.Tests
--sequenced
177
Morphir.Core.Tests
--maximum-parallel-tests 1
237
Morphir.IR.Pipeline.Plugins.Tests
--sequenced
141
Morphir.IR.Pipeline.Tests
--sequenced
299
Morphir.Internal.CodeGeneration.Tests
--sequenced
25
Morphir.Ion.Tests
--sequenced
463
Morphir.Models.Tests
--sequenced
791
Morphir.Myriad.Integration.Tests
--maximum-parallel-tests 1
12
Morphir.Myriad.Tests
--sequenced
4
Morphir.SDK.CSharp.Tests
--maximum-parallel-tests 1
20
Morphir.SDK.Conformance.Tests
--maximum-parallel-tests 1
27
Morphir.SDK.Tests
--sequenced
403
Morphir.Tooling.Tests
--maximum-parallel-tests 1
433
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.
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:
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: 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:
Collect: Gather all “Patterns Discovered” sections from the quarter
Analyze: Identify top 3-5 patterns by frequency
Decide: Determine which patterns warrant automation (Myriad plugin, script, or decision tree update)
Document: Update the guru’s pattern catalog and playbooks
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
Savings: 845 tokens per review × 4 reviews/quarter = 3,380 tokens per quarter
Total Token Savings (All Scripts)
Script
Per-Use Savings
Frequency
Annual Savings
extract-elm-tests.fsx
420 tokens
80 modules
33,600 tokens
analyze-elm-module.fsx
555 tokens
80 modules
44,400 tokens
verify-migration.fsx
765 tokens
80 modules
61,200 tokens
detect-patterns.fsx
845 tokens
4/quarter × 4 quarters
13,520 tokens
Total Annual Savings
152,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)
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
Aspect
Proactive Review
Reactive Retrospective
Timing
Scheduled (weekly, quarterly) or continuous
After events (failures, releases)
Focus
Find issues before they cause problems
Understand why problems occurred
Input
Scans, metrics, automated analysis
Incidents, failures, team feedback
Output
Findings, recommendations, metrics
Root causes, lessons learned
Action
Preventive 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
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.
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
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)
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"
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"
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 testsdotnet test --nologo
# Run with coveragedotnet test --collect:"XPlat Code Coverage"# Run specific test projectdotnet test tests/Morphir.Core.Tests
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.
awaitusing 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:
Option
Default
Use
Isolated
true
Set to false for a test that exercises machine or personal configuration. Write those files inside the sandbox, for example under sandbox.MorphirHome.
RedirectHome
true
Set to false only for a child that needs the real SDK state. Morphir stays contained either way.
Boundary
sandbox root
Bound 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:
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
Verify Morphir.Tool project is correctly configured as a dotnet tool
Validate build system refactoring using vertical slice architecture
Confirm deprecated code removal without breaking existing functionality
Test CI workflow simulation targets work locally
Verify package generation for all four packages (Core, Tooling, Morphir, Tool)
Validate Windows build fixes resolve file locking issues
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)
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
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
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"
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.targetsgrep -i "GenerateWolverineCode" Directory.Build.targets
# 2. Verify GenerateWolverineCode exists in Build.Testing.csgrep "GenerateWolverineCode" build/Build.Testing.cs
# 3. Verify parallel builds enabledgrep "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 descriptiongrep -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 documentationgrep -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 REMOVEDgrep -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 feeddotnet tool uninstall -g Morphir.Tool || true
dotnet tool install -g Morphir.Tool --add-source artifacts/local-feed
# 5. Verify tool worksdotnet-morphir --version
# 6. Cleanupdotnet 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.
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
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:
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
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
Risk
Likelihood
Impact
Mitigation
Windows file locking returns
Low
High
Root cause fixed; monitor CI
Helper classes needed later
Medium
Low
Can add incrementally when needed
Tool naming confusion
Low
Medium
Documentation clear on dotnet-morphir
Morphir packable breaks AOT
Low
High
Tested 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 structurels -la src/Morphir.Tool/
ls -la build/Build*.cs
# 2. Build all packages./build.sh PackAll
# 3. Verify packagesls -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 testsset -euo pipefail
echo "=== Phase 1 Automated Test Suite ==="echo ""# PST-001echo "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-003echo "PST-003: Build System Split Verification"test $(ls build/Build*.cs | wc -l) -eq 5grep -q 'partial.*class Build' build/Build.cs
echo "✓ PST-003 passed"echo ""# BT-001echo "BT-001: PackTool Target Test"./build.sh PackTool
test -f artifacts/packages/Morphir.Tool.*.nupkg
echo "✓ BT-001 passed"echo ""# BT-002echo "BT-002: PackAll Target Test"./build.sh Clean
./build.sh PackAll
test $(ls artifacts/packages/*.nupkg | wc -l) -eq 4echo "✓ BT-002 passed"echo ""# INT-002echo "INT-002: Existing Tests Still Pass"./build.sh Test
echo "✓ INT-002 passed"echo ""echo "=== All automated tests passed ==="
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.
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
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 verificationcd 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.
✅ 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”)
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
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 Results1.**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
Part
Location
The cases (generated Gherkin)
conformance/suites/wasm-host/, from tests/Fixtures/MepWasm/wasm-host.cases.json
The worker adapter, comparer, judge, report and check
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.
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.
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:
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:
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.
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.
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.
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.
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.
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:
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:
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:
ProfileSelection.Names supplied by the host.
A present MORPHIR_PROFILES environment entry, including an empty value.
The discovered root primary documents’ active selector.
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
Environment
Evidence
Windows arm64, .NET 10.0.14, SDK 10.0.403 Roslyn driver
Windows arm64, .NET 10.0.14, SDK 10.0.403 fresh in-memory consumer
Both 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=link
Published consumer executes with identical observations/source hashes to its untrimmed build; no IL warnings in this smoke
Legacy .NET Framework / netstandard IDE compiler hosts
Untested; Models and core require net10.0
Other IDE/runtime/OS combinations
Untested
Native AOT
Untested
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 type
C# representation
Basics Int, Float, Bool
int, double, bool
String, Char, Decimal
string, char, exact admitted decimal
Unit, Order
Morphir.SDK.CSharp.Unit, Order
List, Maybe, Result
SDK immutable wrappers, Result<E,V> with error first
Dict, Set
SDK immutable wrappers with Morphir comparers
Tuple with at least two items
C# value tuple
Function
Nested System.Func<Input,Output>
Closed record
Sealed class with constructor and get-only payloads
Custom type
Closed 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 module
Admitted runtime calls
Basics
Checked Int32/IEEE Float arithmetic, conversion/rounding, comparisons, min/max/clamp, Order cases and structural equality
List
singleton, cons, map, append, head, tail and length
Maybe
Just/Nothing constructors and patterns, map and andThen
Result
Ok/Err constructors and patterns, map and andThen; error type first, value type second
Dict
empty, singleton, fromList, get, insert, update, remove, toList and union
Set
empty, singleton, fromList, insert, remove, toList and union
Decimal
Exact 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.
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.
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.
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:
Claim
Value
Claims version
0.1.0-draft.2
Extension id and type
org.finos.morphir.fsharp-backend, backend
Backend target
fsharp, for IR 4.0.0 and 3.0.0
Legacy morphir.backend.generate
no
Model tuples
morphir.backend.generateModel, profile morphir-ir, codec morphir-json, envelope 0.1.0-draft.1, IR 4.0.0 and 3.0.0
Guarantees
none
Streaming, cancellation, progress
no
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:
--project-file <file> on the command line;
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.
Code
Exit
Meaning
sources.unknown_project_file
2
No provider recognizes the file. The message names the file and the registered patterns.
sources.ambiguous_project_file
2
More than one provider recognizes the file. The message names them.
sources.missing
2
The 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:
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 content
Result
<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.
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.