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

Return to the regular view of this page.

CLI Reference

Command-line interface reference for Morphir .NET tooling

Morphir .NET CLI Reference

The Morphir .NET CLI provides powerful command-line tools for working with Morphir IR files, validating schemas, and managing Morphir projects.

Available Commands

Toolchain manager

  • morphirup - The standalone toolchain manager. This build makes plan-only setups and checks a project’s request and lock; it acquires and installs nothing yet

IR Management

  • morphir ir verify - Validate Morphir IR JSON files against official schemas
  • morphir ir detect-version (coming in Phase 2) - Detect the schema version of an IR file

Project Management

These migration commands are not yet implemented.

Installation

The Morphir .NET CLI is distributed as a .NET tool. Install it globally with:

dotnet tool install -g Morphir.CLI

Or locally in a project:

dotnet tool install Morphir.CLI

Getting Help

For help with any command, use the --help flag:

morphir --help
morphir ir --help
morphir ir verify --help

Common Workflows

Validating IR Files

The most common workflow is validating Morphir IR JSON files to ensure they conform to the expected schema:

# Validate a single file with auto-detection
morphir ir verify path/to/morphir-ir.json

# Validate with explicit schema version
morphir ir verify --schema-version 3 path/to/morphir-ir.json

# Get JSON output for CI/CD
morphir ir verify --json path/to/morphir-ir.json

# Quiet mode (only errors)
morphir ir verify --quiet path/to/morphir-ir.json

See the morphir ir verify documentation for complete details.

Exit Codes

All Morphir CLI commands use consistent exit codes:

  • 0: Success - operation completed successfully
  • 1: Validation failure - IR file failed schema validation
  • 2: Operational error - file not found, invalid JSON, missing dependencies, etc.

These exit codes are designed for CI/CD integration and scripting.

Configuration

The configuration guide distinguishes the compatibility TOML cache loader from the additive shared configuration and project system.

The morphir config read commands inspect effective/scoped values, source reports and redacted explanations. JSON output follows the published schema; success, missing keys and usage/loading failures use exit codes 0/1/2. Shared config set/unset editing, project migrate and bounded workspace migrate are available. Start with the workspace configuration walkthrough.

Troubleshooting

See the Troubleshooting Guide for solutions to common issues.

1 - morphir ir verify

Validate Morphir IR JSON files against official schemas

morphir ir verify

Validate Morphir IR JSON files against the official JSON schemas for format versions 1, 2, and 3.

Synopsis

morphir ir verify <file-path> [options]

Description

The morphir ir verify command validates a Morphir IR JSON file against the appropriate schema specification. The command automatically detects the schema version from the file content, or you can explicitly specify the version to use.

This command is useful for:

  • Catching structural errors before IR files are used by other tools
  • Validating generated IR from Morphir compilers
  • CI/CD integration to ensure IR quality
  • Debugging IR issues with detailed error messages

Arguments

<file-path>

Required. Path to the Morphir IR JSON file to validate.

  • Supports absolute and relative paths
  • File must exist and be readable
  • File must contain valid JSON

Examples:

morphir ir verify morphir-ir.json
morphir ir verify ../output/morphir-ir.json
morphir ir verify /absolute/path/to/morphir-ir.json

Options

--schema-version <version>

Explicitly specify the schema version to validate against.

  • Valid values: 1, 2, or 3
  • Default: Auto-detected from file content
  • Use when: Testing version-specific compatibility or overriding auto-detection

Examples:

# Validate against v3 schema regardless of file content
morphir ir verify --schema-version 3 morphir-ir.json

# Test if a v2 IR file is compatible with v3 schema
morphir ir verify --schema-version 3 morphir-ir-v2.json

--json

Output validation results in JSON format instead of human-readable text.

  • Default: Human-readable output
  • Use when: Parsing results in CI/CD, scripts, or other tools
  • Output: Structured JSON with validation details

JSON Output Format:

{
  "IsValid": true,
  "SchemaVersion": "3",
  "DetectionMethod": "auto",
  "FilePath": "/path/to/morphir-ir.json",
  "Errors": [],
  "Timestamp": "2025-12-15T10:30:00Z"
}

For invalid IR:

{
  "IsValid": false,
  "SchemaVersion": "3",
  "DetectionMethod": "auto",
  "FilePath": "/path/to/morphir-ir.json",
  "Errors": [
    {
      "Path": "$.distribution[3]",
      "Message": "Required properties [\"formatVersion\"] are not present",
      "Expected": "required property",
      "Found": "undefined (missing)",
      "Line": null,
      "Column": null
    }
  ],
  "Timestamp": "2025-12-15T10:30:00Z"
}

Example:

# Get JSON output for parsing
morphir ir verify --json morphir-ir.json

# Use in scripts
RESULT=$(morphir ir verify --json morphir-ir.json)
echo $RESULT | jq '.IsValid'

--quiet

Suppress all output except for errors.

  • Default: Show detailed output
  • Use when: Running in CI/CD pipelines where you only care about failures
  • Exit code: Still returns 0 (success) or 1 (failure)

Examples:

# Quiet mode - only shows output if validation fails
morphir ir verify --quiet morphir-ir.json

# Use in CI/CD
if morphir ir verify --quiet morphir-ir.json; then
  echo "IR is valid"
else
  echo "IR validation failed"
  exit 1
fi

Exit Codes

CodeMeaningDescription
0SuccessIR file is valid according to the schema
1Validation failureIR file failed schema validation (see error output)
2Operational errorFile not found, malformed JSON, or other operational issue

Output

Human-Readable Output (Default)

Valid IR:

Validation Result: ✓ VALID
File: /path/to/morphir-ir.json
Schema Version: v3 (auto)
Timestamp: 2025-12-15 10:30:00 UTC

No validation errors found.

Invalid IR:

Validation Result: ✗ INVALID
File: /path/to/morphir-ir.json
Schema Version: v3 (auto)
Timestamp: 2025-12-15 10:30:00 UTC

Found 2 validation error(s):

  Path: $.distribution[3]
  Message: Required properties ["formatVersion"] are not present
  Expected: required property
  Found: undefined (missing)

  Path: $.distribution[3].modules[0].name
  Message: Value is "string" but should be "array"
  Expected: array
  Found: string

JSON Output

See the --json option above for the JSON output format specification.

Schema Version Detection

The command automatically detects the schema version by analyzing the IR file structure:

  • v1: Detected by presence of v1-specific structure
  • v2: Detected by "formatVersion": 2 field
  • v3: Detected by "formatVersion": 3 field

Detection Method in Output:

  • auto: Version was automatically detected
  • manual: Version was explicitly specified via --schema-version

Examples

Basic Validation

Validate a single IR file with auto-detection:

morphir ir verify morphir-ir.json

Explicit Version

Validate against a specific schema version:

morphir ir verify --schema-version 3 morphir-ir.json

CI/CD Integration

Validate IR in a GitHub Actions workflow:

- name: Validate Morphir IR
  run: |
    dotnet morphir ir verify --json morphir-ir.json > validation-result.json
    if [ $? -ne 0 ]; then
      cat validation-result.json
      exit 1
    fi

Script with Error Handling

#!/bin/bash
set -e

echo "Validating Morphir IR files..."

for file in output/*.json; do
  echo "Validating $file..."
  if morphir ir verify --quiet "$file"; then
    echo "✓ $file is valid"
  else
    echo "✗ $file is invalid"
    morphir ir verify "$file"  # Show detailed errors
    exit 1
  fi
done

echo "All IR files are valid!"

JSON Output Parsing

Parse JSON output with jq:

# Check if valid
morphir ir verify --json morphir-ir.json | jq '.IsValid'

# Get error count
morphir ir verify --json morphir-ir.json | jq '.Errors | length'

# Extract error messages
morphir ir verify --json morphir-ir.json | jq '.Errors[].Message'

# Get schema version used
morphir ir verify --json morphir-ir.json | jq '.SchemaVersion'

Common Error Messages

File Not Found

Error: File does not exist: path/to/file.json

Solution: Check the file path and ensure the file exists.

Malformed JSON

Validation Result: ✗ INVALID

Found 1 validation error(s):

  Path: $
  Message: Malformed JSON: 'i' is an invalid start of a value. LineNumber: 6 | BytePositionInLine: 4.
  Expected: Valid JSON
  Found: Invalid JSON syntax

Solution: Fix the JSON syntax error. The error message includes the line and byte position.

Missing Required Field

Path: $.distribution
Message: Required properties ["formatVersion"] are not present
Expected: required property
Found: undefined (missing)

Solution: Add the missing formatVersion field to your IR file.

Type Mismatch

Path: $.distribution[3].modules[0].name
Message: Value is "string" but should be "array"
Expected: array
Found: string

Solution: Change the field type to match the schema requirement (in this case, name should be an array, not a string).

Troubleshooting

Schema Version Not Detected

If auto-detection fails or detects the wrong version:

  1. Check the formatVersion field in your JSON (for v2/v3)
  2. Use --schema-version explicitly to override auto-detection
  3. Verify JSON structure matches the expected schema version

Performance Issues

For large IR files (>1MB):

  • Validation may take several seconds
  • Consider using --quiet mode in CI/CD to reduce output overhead
  • (Phase 2 will include performance optimizations for batch processing)

False Positives

If validation fails but you believe the IR is correct:

  1. Check schema version - ensure you’re validating against the correct version
  2. Review error messages - they include expected vs. found values
  3. Consult schema documentation - see Schema Specifications
  4. Report an issue - if you believe the schema or validator has a bug
  • morphir ir detect-version (Phase 2) - Detect schema version without validation
  • morphir ir migrate (Phase 3) - Migrate IR between schema versions

See Also

2 - Management Commands

Commands for managing Morphir distributions, tools, and extensions.

Overview

The Morphir CLI provides commands to manage distributions, tools, and extensions with support for:

  • Global-first installation (in OS config directory like ~/.config/morphir or ~/.morphir)
  • Local overrides (per-project in .morphir/ directory)
  • Platform-specific artifacts using .NET Runtime Identifiers (e.g., linux-x64, win-x64, osx-arm64)

Dist Commands

Manage core Morphir distributions.

dist list

List installed distributions.

morphir dist list [options]

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: List local (project) installations only

Example:

# List all distributions for current platform
morphir dist list

# List distributions for a specific platform
morphir dist list --platform linux-x64

# List project-local distributions
morphir dist list --local

dist install

Install a distribution from a URL.

morphir dist install <url> <version> [options]

Arguments:

  • <url>: Source URL to download the distribution from
  • <version>: Version string (e.g., 1.0.0, 2.1.3-beta)

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Install locally (project-specific)

Example:

# Install globally
morphir dist install https://example.com/morphir-dist.tar.gz 1.0.0

# Install locally for project
morphir dist install https://example.com/morphir-dist.tar.gz 1.0.0 --local

# Install for specific platform
morphir dist install https://example.com/morphir-dist.tar.gz 1.0.0 --platform linux-x64

dist use

Set the active distribution version.

morphir dist use <version> [options]

Arguments:

  • <version>: Version to activate

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Set in local (project) scope

Example:

# Set global active version
morphir dist use 1.0.0

# Set local active version for current project
morphir dist use 2.0.0 --local

Resolution: Local selection takes precedence over global when both are set.

dist remove

Remove an installed distribution.

morphir dist remove <version> [options]

Arguments:

  • <version>: Version to remove

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Remove from local (project) scope

Example:

morphir dist remove 1.0.0
morphir dist remove 1.0.0 --local

dist which

Show the currently active distribution.

morphir dist which [options]

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)

Example:

morphir dist which
# Output: Version 1.0.0 (global) at /home/user/.config/morphir/dist/linux-x64/1.0.0

Tool Commands

Manage auxiliary CLI tools and utilities.

tool list

List installed tools.

morphir tool list [options]

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: List local (project) installations only

tool install

Install a tool from a URL.

morphir tool install <name> <url> <version> [options]

Arguments:

  • <name>: Tool name
  • <url>: Source URL
  • <version>: Version string

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Install locally (project-specific)

Example:

morphir tool install morphir-elm-codegen https://example.com/tool.tar.gz 1.0.0
morphir tool install my-formatter https://example.com/formatter.tar.gz 2.1.0 --local

tool use

Set the active tool version.

morphir tool use <name> <version> [options]

Arguments:

  • <name>: Tool name
  • <version>: Version to activate

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)
  • --local: Set in local (project) scope

tool remove

Remove an installed tool.

morphir tool remove <name> <version> [options]

tool which

Show the currently active tool version.

morphir tool which <name> [options]

Options:

  • --platform <rid>: Platform Runtime Identifier (defaults to current platform)

Extension Commands

extension list and explicit local-developer installation are available. Installation uses a registered repository name, a release declaration and a local archive, with explicit unchecked/no-probe consent. A positional archive URL is not accepted. Selection and removal commands remain stubs. See extension installation status. The full installer is tracked by #558.

An extension adds a capability to Morphir, such as a code generator. The plan is in the Morphir extension model PRD.

morphir extension list [--json] [--platform <rid>] [--local]
morphir extension runtimes [--json]
morphir extension repository add <name> --directory <absolute-path> --home <absolute-home> --local-developer [--dry-run]
morphir extension install <name> --repository <registered-name> --local-release <json> --local-archive <zip> --home <absolute-home> --local-developer --no-probe [--dry-run]
morphir extension use <name> <version> [--platform <rid>] [--local]             # not implemented yet
morphir extension remove <name> <version> [--platform <rid>] [--local]          # not implemented yet
morphir extension which <name> [--platform <rid>]                               # not implemented yet

use, remove and which fail with exit code 1 and a link to issue #492. They arrive with step 3 of the PRD.

morphir extension runtimes reports whether this host can run WASM extensions, with its engine, profiles and bounds, or the reason it cannot. See morphir extension runtimes.

morphir extension worker is internal and experimental, and is not in this list or in the help: it serves one WASM module over stdio for a conformance runner, and no product command uses it. See morphir extension worker.

extension list

List the extensions that this host knows, and say which offering of morphir-dotnet is running and what it needs from the machine. Today the extensions are the built-in F# frontend of morphir compile, the built-in generators of morphir generate and the built-in analyzers of morphir validate.

$ morphir extension list
╭────────────────────────┬────────────────┬─────────┬─────────────────────┬──────────┬─────────────────────────╮
│ ID                     │ NAME           │ VERSION │ TYPES               │ DELIVERY │ HOSTS                   │
├────────────────────────┼────────────────┼─────────┼─────────────────────┼──────────┼─────────────────────────┤
│ morphir-dotnet.fsharp  │ F# frontend    │ 0.4.0   │ frontend (fsharp)   │ built-in │ morphir, dotnet-morphir │
│ morphir.csharp         │ C# generator   │ 0.4.0   │ backend             │ built-in │ morphir, dotnet-morphir │
│ morphir.fsharp         │ F# generator   │ 0.4.0   │ backend             │ built-in │ morphir, dotnet-morphir │
│ morphir.type-validator │ Type validator │ 0.4.0   │ validator (1, 2, 3) │ built-in │ morphir, dotnet-morphir │
╰────────────────────────┴────────────────┴─────────┴─────────────────────┴──────────┴─────────────────────────╯

The rows are sorted by id. The version of a built-in is the version of the host, including any build metadata. The TYPES column shows each type. For a frontend, the languages that it reads follow in parentheses; for a validator, the IR versions that it reads.

In a terminal the table has styled borders and headers and fits the terminal width. When stdout is redirected, or NO_COLOR is set, the table has no ANSI codes. The table is for people: scripts use --json.

Options:

  • --json: Write one JSON document on stdout.
  • --platform <rid>, --local: Select installed extensions. There are none until step 3 of the PRD, so these options do not change the result now. Built-in extensions always appear.

With --json, from the Native AOT morphir for Windows ARM64:

{
  "host": "morphir",
  "deployment": "native-aot",
  "execution": "native-aot",
  "platform": "win-arm64",
  "prerequisites": { "runtime": [], "sdk": [], "native": [] },
  "extensions": [
    {
      "id": "morphir-dotnet.fsharp",
      "name": "F# frontend",
      "version": "0.4.0",
      "description": "Compiles F# source files to a Morphir model in process.",
      "types": ["frontend"],
      "delivery": "built-in",
      "hosts": ["morphir", "dotnet-morphir"],
      "deployments": ["self-contained", "framework-dependent", "dotnet-tool"],
      "available": false,
      "reason": "The native-aot morphir has no in-process F# frontend; the self-contained and framework-dependent morphir and dotnet-morphir have one. Here, install the F# frontend process delivery morphir-fsharp-frontend and configure it with [frontend.fsharp].extension or --extension.",
      "prerequisites": { "runtime": [], "sdk": [], "native": [] },
      "capabilities": {
        "frontend": {
          "languages": [{ "id": "fsharp", "fileExtensions": [".fs", ".fsi"] }],
          "irVersions": ["1", "2", "3", "4"]
        }
      }
    },
    {
      "id": "morphir.csharp",
      "name": "C# generator",
      "version": "0.4.0",
      "description": "Generates C# source from a Morphir library model.",
      "types": ["backend"],
      "delivery": "built-in",
      "hosts": ["morphir", "dotnet-morphir"],
      "deployments": ["native-aot", "self-contained", "framework-dependent", "dotnet-tool"],
      "available": true,
      "prerequisites": { "runtime": [], "sdk": [], "native": [] },
      "capabilities": {
        "backend": { "targets": ["csharp"], "irVersions": ["1", "2", "3", "3.0.0", "3.1.0", "4", "4.0.0"], "generate": true }
      }
    }
  ]
}

The example leaves out the morphir.fsharp and morphir.type-validator entries. The four offerings report:

OfferinghostdeploymentexecutionplatformTop-level prerequisitesmorphir-dotnet.fsharp
Native AOT morphirmorphirnative-aotnative-aotthe RIDnonenot available, with the reason above
Self-contained morphirmorphirself-containedjitthe RIDnoneavailable
Framework-dependent morphirmorphirframework-dependentjitthe RIDruntime Microsoft.NETCore.App >=10.0.0 <11.0.0available
dotnet-morphir (Morphir.Tool)dotnet-morphirdotnet-tooljitleft out: the package is portablethat runtime, and SDK Microsoft.NET.Sdk >=10.0.100 with "phase": "install"available

A build that no named release profile produced, such as dotnet run, a test host, or the legacy morphir-<rid> single-file assets that releases attach today, has no qualified deployment: it leaves out deployment, platform and the top-level prerequisites, and never reports one of the four names. It still reports execution.

FieldMeaning
hostThe command that wrote the list: morphir or dotnet-morphir (the dotnet tool). It is a command name, not the offering: the Native AOT, self-contained and framework-dependent offerings all report morphir.
deploymentThe offering that the build’s named release profile set at build time: native-aot, self-contained, framework-dependent or dotnet-tool. It is never inferred from how the process runs. Left out when no named profile built the executable.
executionHow this process runs: native-aot or jit.
platformThe .NET runtime identifier that the profile built for. Left out for the portable tool package and when no named profile built the executable.
prerequisitesWhat the offering needs from the machine: runtime (an installed .NET runtime, to run), sdk (a .NET SDK, with phase install or run) and native (none is declared yet). Left out when no named profile built the executable.
deploymentsThe offerings in which the extension is available.
reasonWhy the extension is not available in the running offering. Present exactly when available is false.
extension prerequisitesWhat the extension needs from the machine beyond the host. Every built-in needs nothing.
idThe extension id. See Extension ids. For a built-in generator, it is also the backend value in morphir generate --json reports.
versionCanonical SemVer 2.0.0.
descriptionLeft out when the extension has none.
typesMEP extension types: frontend, backend, transform, validator or workspace. They come from the capabilities.
deliveryHow the extension is delivered: built-in (compiled into the host) or wasm (a WASM module that the host’s WASM runtime runs). Every entry listed today is built-in: listing configured WASM modules is planned in issue #565.
hostsThe hosts that the extension runs in.
availabletrue when the extension runs in this host and offering. An extension for the other host, or for other offerings, is listed with false and a reason. A wasm extension in a host whose WASM runtime is unavailable is listed with false.
capabilities.frontendThe MEP frontend claim: the languages with their file extensions, and the IR versions that it produces.
capabilities.backendThe MEP backend claim: targets, IR versions in their declared spelling, and generate.
capabilities.validatorThe IR versions that a validator reads, in the spelling of the model readers. This is a morphir-dotnet report member: MEP 0.2.0-draft.1 has no validator capability, so hosts do not write it into MEP messages.

Logs and diagnostics go to stderr, so morphir extension list --json | jq works. If two extensions have the same id, the command exits with code 2 and names each duplicate id (extension.duplicate_id). With --json, the document then has an empty extensions array and a diagnostics array.

Extension ids

An extension id is one or more segments joined by dots, at most 238 characters. Each segment starts with a lowercase ASCII letter, contains only lowercase letters, digits and -, and ends with a letter or digit. Examples: morphir.csharp, acme.tools.lint-2, morphir-elm.

A one-segment id follows the upstream Morphir rule. Dotted ids are a morphir-dotnet superset, proposed upstream in finos/morphir#1137.

Every id that starts with morphir. is reserved for the built-in frontends and backends of morphir-dotnet, including built-ins that do not ship yet. compile and generate refuse an extension guest that reports such an id (extension.unavailable). Ids such as org.finos.morphir.fsharp-backend are not reserved.

Platform Identifiers (RIDs)

Morphir uses .NET Runtime Identifiers to manage platform-specific artifacts:

PlatformRID
Linux x64linux-x64
Linux ARM64linux-arm64
Windows x64win-x64
Windows ARM64win-arm64
macOS x64 (Intel)osx-x64
macOS ARM64 (Apple Silicon)osx-arm64

The CLI automatically detects the current platform when --platform is not specified.

Resolution Rules

When looking for an active artifact, the CLI uses the following precedence:

  1. Project-local selection (if .morphir/ exists and selection is set locally)
  2. Global selection (in OS config directory)
  3. Error if no selection is found

Each artifact category (dist, tool, extension) resolves independently.

Folder Layout

Global (preferred):
  ~/.config/morphir/           (Linux)
  ~/Library/Application Support/morphir/  (macOS)
  %APPDATA%\Morphir\           (Windows)
    dist/
      <platform>/
        <version>/
          bin/
          manifest.json
    tools/
      <platform>/
        <tool-name>/
          <version>/
            bin/
            manifest.json
    extensions/
      <platform>/
        <extension-name>/
          <version>/
            bin/
            manifest.json

Local (optional, per project):
  .morphir/
    dist/...
    tools/...
    extensions/...

Manifest Format

Each installed artifact includes a manifest.json file (JSONC format with comments supported):

{
  "name": "morphir-dist",
  "version": "1.0.0",
  "platform": "linux-x64",
  "sourceUrl": "https://example.com/artifact.tar.gz",
  "sha256": "abc123...",
  "description": "Core Morphir distribution",
  "installedAt": "2025-12-23T19:00:00Z",
  "metadata": {
    "key": "value"
  }
}

See Also

3 - Config commands

Inspect and edit Morphir configuration through shared services

Config commands

morphir config get|list|show|path|explain reads configuration through the same shared APIs available to embedded hosts. It never edits project files, executes scripts, or resolves secret references. Existing IR and artifact-management commands keep their own configuration behavior; these inspection commands do not make them consume every setting.

Synopsis

morphir config get <key> [options]
morphir config list [options]
morphir config show [options]
morphir config path [options]
morphir config explain <key> [options]
morphir config set <key> <value> [--type <type>] [--dry-run] [options]
morphir config unset <key> [--dry-run] [options]
CommandResult
get <key>One typed value; a missing key is distinct from an empty string or array
listOrdered canonical key/value entries; arrays are complete values
showThe nested document
pathConsulted sources with status, role, format and ordering metadata
explain <key>Contributing layers, the effective value and winning origins

Keys are case-sensitive. Dots separate segments; JSON-escaped double-quoted segments select literal dots or special characters. Protect the complete path with shell single quotes:

morphir config get ir.format_version
morphir config get 'codegen."custom.generator".enabled' --json
morphir config list --show-origin --show-scope
morphir config show --json
morphir config path --json
morphir config explain custom.theme

Arrays are queried as a whole, without indexing syntax. custom.items.0 does not select an array element. Empty unquoted segments, trailing dots and unterminated quoted segments are usage failures.

Scope selectors

Effective configuration is the default. A scope selects its own sources before merging and never fills a missing key from defaults, environment values or another scope.

SelectorSources
--workspaceWorkspace primary files
--project <name-or-path>Primary files of one selected project; a relative path starts at the workspace root
--userPersonal overrides adjacent to the selected project or root
--globalGlobal user files
--systemSystem files
--file <path>One explicitly selected supported-format file

Selectors are mutually exclusive. --file and --config cannot be combined. --file reads a required file, even when it was not a discovered primary. --config overlays a required selected file during effective resolution. Relative file paths start at the invocation directory. Neither option writes anything.

An invalid workspace.default_member produces a selection error for effective, project and adjacent-user queries. Workspace, global, system and file scopes inspect their own sources independently of automatic project selection. File scope also excludes synthetic classic compatibility defaults, so an unauthored pipeline.backend remains missing there.

A workspace-root invocation reads the root unless shared selection chooses one default project. Invocation inside a member reads that member with applicable inherited settings. Use --workspace or an explicit project selector when inspecting a particular primary scope. Ambiguous or unknown project selectors fail visibly.

Resolution and output options

OptionPurpose
--show-originInclude winning paths, formats and available positions with entries
--show-scopeInclude winning source roles with entries
--jsonEmit one JSON report with typed JSON values and structured diagnostics
--config <path>Overlay an explicitly selected configuration file
--discovery-boundary <directory>Include this directory in discovery and exclude its parents. Default: MORPHIR_DISCOVERY_BOUNDARY, else the current filesystem volume
--profiles <names>Comma-separated profile names, repeatable in order
--no-profilesDisable profiles; conflicts with --profiles
--isolated [true|false]Exclude machine and personal files; retain active profiles and supplied environment values. Default: MORPHIR_ISOLATED, else false. --isolated false turns off isolation set in the environment
--suppress-warning <code>Suppress a supported warning, repeatable
--helpShow help without configuration discovery or loading

Profile names match [a-z][a-z0-9_-]{0,63}. Invalid explicit or environment profile names fail before discovery. --profiles ci --profiles metrics activates ci then metrics; use --no-profiles to disable activation. The supported mixed-format warning codes are config.yaml_beside_toml and config.json_keys_below_toml. Unknown suppression codes produce a structured warning.

Without an explicit boundary, the native CLI searches within the current filesystem volume. It captures native machine locations and environment values at execution time. Machine paths use their own filesystem roots, including other Windows drives and redirected UNC shares, independently of the project discovery boundary. Embedded hosts can instead supply bounded or mounted trees without ambient machine access. See the configuration guide.

Containment from the environment

A parent process, CI job or test harness can contain every config command without changing its arguments:

VariableMirrorsAccepted values
MORPHIR_DISCOVERY_BOUNDARY--discovery-boundaryA directory. A relative path resolves against the current directory. Native Windows drive and UNC paths work with either separator
MORPHIR_ISOLATED--isolatedtrue, false, 1 or 0, in any case

A flag on the command line wins over its variable, and the variable wins over the default. An empty variable counts as unset. Neither variable ever becomes a configuration value.

The command fails with exit code 2, before reading any configuration, when:

  • MORPHIR_ISOLATED has any other value. The containment.invalid_value diagnostic names the variable and the accepted values.
  • The current directory is outside the effective boundary. The discovery.boundary diagnostic names the boundary and where it came from, so an inherited variable is easy to spot.

--help never reads either variable.

$ export MORPHIR_DISCOVERY_BOUNDARY=$PWD MORPHIR_ISOLATED=1
$ morphir config path --json

config path --json reports what was in effect:

"containment": {
  "boundary": "/home/ada/repo",
  "boundarySource": "environment",
  "isolated": true,
  "isolationSource": "environment"
}

Each source is flag, environment or default.

JSON contract

Every read command emits one object with these fields. The JSON Schema is checked against subprocess results in the CLI behavior suite. No F# union or reflection-based serialization is exposed.

FieldType and meaning
operationget, list, show, path or explain
scopeeffective, workspace, project, user, global, system or file:<canonical-path>
keyCanonical quoted key path, or null
foundBoolean; distinguishes missing keys from present empty values
valueJSON scalar, array or object; null for a missing key or an operation without a value
entriesArray of {key, value, origins}
sourcesArray of {source, status, contributed, detail}
contributionsArray of {source, value, winner, origins}
diagnosticsArray of {code, severity, message, path, key, line, column}
valueTypesArray of {pointer, kind, encoding} for canonical types requiring JSON metadata
exitCodeInteger 0, 1 or 2
containmentpath only: {boundary, boundarySource, isolated, isolationSource}; omitted by other operations

A source is {path, format, role, order, profile}. format is json, yaml, toml or memory; role uses the canonical source-role name such as RootPrimary, MemberPrimary, Global or Environment. profile is a name or null. Source status is loaded, missing, skipped or failed; optional missing candidates remain in the source inventory.

An origin is {key, source, line, column}. Origins are empty unless requested for get/list/show entries; explanations always include them. With only --show-scope, path/format are empty and positions are null; with only --show-origin, the role is empty. order and profile metadata remain available. Composite values can have multiple winning origins.

For example, querying a present empty array returns a successful report containing:

{
  "operation": "get",
  "scope": "effective",
  "key": "custom.targets",
  "found": true,
  "value": [],
  "entries": [{ "key": "custom.targets", "value": [], "origins": [] }],
  "sources": [],
  "contributions": [],
  "diagnostics": [],
  "valueTypes": [],
  "exitCode": 0
}

Reports preserve the readers’ supported 128 levels of configuration nesting. Consumers using System.Text.Json should set MaxDepth = 256 when parsing deeply nested reports, allowing room for the report envelope and typed value tags.

The sources array above is abbreviated to emphasize the value contract. path and other result source inventories retain their actual consulted-source reports. Finite numbers and integers stay JSON numbers, including integers outside Int64. Nonfinite numbers use a tagged value such as {"$number":"NaN"}, {"$number":"Infinity"} or {"$number":"-Infinity"}. The envelope’s separate valueTypes entry identifies the canonical node with kind: "number", encoding: "nonfinite" and a JSON Pointer such as /value/custom/nan. An ordinary user object containing $number has no numeric metadata at that pointer and remains an object. This avoids reserving user keys.

For example these two value fragments have different canonical types:

{
  "value": {"$number": "NaN"},
  "valueTypes": [{"pointer": "/value", "kind": "number", "encoding": "nonfinite"}]
}
{
  "value": {"$number": "NaN"},
  "valueTypes": []
}

The first represents a nonfinite number. The second is an ordinary object. The complete report also carries the other required envelope fields listed above. Positive and negative infinity use the tags Infinity and -Infinity with the same external discriminator. Consumers use the metadata pointer, never user keys alone, to recognize special values.

Temporal values retain their string text and have kind: "temporal" metadata with the original temporal kind in encoding, for example LocalDate. Metadata also identifies special values under /entries/0/value and /contributions/0/value. Pointers use standard JSON Pointer escaping, including ~0 for ~ and ~1 for /. Finite values need no metadata. Redaction replaces sensitive values and secret references with "[REDACTED]" before presentation and before special-value encoding; safe key/source metadata remains inspectable. Diagnostics retain structured codes and metadata while avoiding raw secret-bearing input text.

Stdout contains only results. Human diagnostic text goes to stderr in both human and JSON modes. Successful JSON with mixed-format warnings therefore remains parseable, and diagnostics carries the warnings for scripts. Usage errors also return one structured JSON report when --json is present.

Exit codes

CodeMeaning
0Successful inspection, preview or edit, including unchanged and overridden writes
1Requested get/explain key or targeted unset entry not found
2Usage, validation, I/O, ambiguous selection, stale-plan conflict or recovery failure

IR verification retains its existing interpretation of code 1.

Malformed or inaccessible host appsettings files return config.load with exit code 2. With --json, the failure remains one JSON result. Invalid command arguments return config.usage before this failure; help does not load host configuration.

CI example

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

Isolation preserves supplied environment values and selected profiles. Control the job’s environment as well when checking reproducibility. An explicitly selected eligible personal file retains its discovered ownership restrictions even under isolation.

Editing one TOML target

set and unset select one file, then use the same reusable planner and writer available to embedded hosts. Without a selector, an invocation inside a project targets its primary. At a declared workspace root it targets the workspace primary, including a virtual workspace. A missing or ambiguous default context fails before writing.

Use one of --workspace, --project <name-or-path>, --user, --global, --system or --file <path>. --user uses the selected project’s adjacent personal file, including a selected default member; a virtual root with no selected project uses its root-adjacent personal file. Existing personal files are considered across every eligible layout of that owner. Multiple personal TOML candidates remain ambiguous. An explicit personal file in another declared member retains that member’s ownership and effective context, including under isolation. Multiple selectors fail. Explicit --file targets must end in .toml. --config remains a selected read overlay and cannot be combined with --file.

The editor uses an existing TOML primary when present. A JSON/YAML-only scope receives the corresponding TOML primary at the same layout. It leaves lower-format files unchanged and refuses a new same-format primary ambiguity. Comments, unrelated entries, key spelling, ordering, line endings, BOM and accepted BOM encodings are preserved. Repeating an identical set does not rewrite the file or update its timestamp.

Strings are the default, even with --json. Explicit types use JSON literal syntax:

--typeInput
stringLiteral command argument, including an empty string
bool or booleantrue or false
int or integerSigned 64-bit JSON integer
numberFinite JSON number, retained as a TOML float
arrayComplete JSON array
objectComplete JSON object

Duplicate object keys, null values and nested nulls fail. Integer input and integer tokens inside composite values must fit Int64; explicit number input chooses finite floating-point precision, including for large literals. Arrays and objects replace the complete target entry. Paths address whole arrays and opaque references; traversal through an atomic ancestor fails. Known settings are validated in the authored target as well as the effective candidate, so a higher environment value cannot hide an invalid edit.

morphir config set ui.color false --type bool --global --dry-run --json
morphir config set custom.caption false --json  # writes the string "false"
morphir config set custom.targets '["csharp"]' --type array
morphir config set custom.generator '{"enabled":true,"targets":["csharp"]}' --type object
morphir config set 'codegen."custom.generator".enabled' false --type bool
morphir config unset custom.caption --user --dry-run

unset removes only the target’s authored entry. It adds no tombstone and never deletes an inherited value from another file. Removing an override can reveal JSON, YAML, another scope or a default. A missing target entry exits 1. A write that remains overridden still exits 0; the report identifies its effective value and winning origins.

Profile, isolation, warning-suppression and discovery-boundary controls retain their read meanings. The explicit edit target is read for editing even when isolation excludes it from the resulting effective view. Independent workspace/global/system/file selectors do not turn an invalid automatic member selection into a target selection. Effective explanations use a valid selected member when available, so a successful workspace write can remain overridden by that member.

Editing JSON reports

Every editing invocation with --json emits one object matching the editing JSON Schema. Stdout contains only this report; safe diagnostics go to stderr.

FieldMeaning
operationset or unset
outcomepreview, written, unchanged, conflict, rolled-back, recovery-required, io-failure or failed
target, keySelected source metadata and canonical quoted path, or null before target selection
before, afterRedacted target-entry values; null represents an absent entry
changed, dryRunWhether the proposed target differs, and whether this is a preview
effectiveThe nested inspection explanation with contributions, origins and its own type metadata
diagnosticsSafe codes and source/key/position metadata
recoveryArtifactsRetained {path, kind, transactionId} records
exitCodeStable outer command category, 0/1/2
valueTypesType metadata for outer before/after values

A nested effective explanation can report a missing value while a successful unset still returns outer exit 0. changed describes the proposed edit; after a conflict or rollback it does not claim that the candidate remains installed. Sensitive names and opaque reference shapes are redacted before rendering, including raw before/after input. No command executes a reference or resolves a secret.

Preconditions and recovery

--dry-run performs no writer acquisition, staging or filesystem change. Application rechecks the plan’s observed source/target identities, discovery inventories and absent alternate candidates under writer coordination. A stale source, destination or alternate-primary observation produces a conflict and leaves the preexisting destination unchanged. Reload, inspect the competing change and create a fresh plan.

Native writes stage on the destination filesystem and atomically install each file. The reusable transaction service supports multiple destinations for future callers, but does not promise group or crash atomicity across files. Generated-output publication and pruning manifests are separate and unchanged.

Native configuration writers use machine-local OS coordination. An embedded host writing a shared store from several machines must supply distributed coordination itself. External editors that do not participate can race with the final filesystem operation; this is an optimistic precondition protocol, not a lock on arbitrary applications.

After a complete rollback, the command exits 2 and cleans its temporary artifacts. An incomplete rollback or cleanup returns typed recovery information with retained paths and ownership. Preserve those files, inspect the target and journal, and restore or reconcile from the matching backup before retrying. Do not remove artifacts from another transaction. See troubleshooting.

Native filesystem access keeps the existing no-follow policy. On macOS, the conventional /etc path is a symlink, so the default system target can return an I/O-category failure before writing. Use an explicitly authorized physical TOML path when needed. A permission failure likewise returns 2; previews do not request elevated access or create directories.

Project migration

Use project migrate to adopt one classic project, or workspace migrate for a bounded repository. Both use loss-checked shared planning and guarded application. Editing does not remove classic manifests, perform migration equivalence checks or adopt workspace membership automatically.

4 - Project migration

Migration of one classic morphir-elm project to the newer Morphir project system

Project migration

morphir project migrate previews and applies one loss-checked conversion. The command calls shared migration services that other hosts can use without invoking the CLI. See the complete command reference for before/after examples, JSON schema and recovery.

Use it to adopt one classic morphir.json project into the newer project system. It does not convert source code, migrate morphir-ir.json, or choose a new IR version. For a repository-wide plan, use workspace migration.

Synopsis

morphir project migrate [<path>] [--output <toml-path>] [--dry-run] [--json]

<path> identifies one project directory or its classic morphir.json. When omitted, select the current unambiguous project. A member invocation does not implicitly migrate its siblings or change workspace membership.

morphir project migrate ./packages/orders --dry-run
morphir project migrate ./packages/orders
morphir project migrate packages/orders/morphir.json --dry-run --json
morphir project migrate morphir.json --output .config/morphir/config.toml

Options

OptionPurpose
--output <path>TOML target in a supported primary layout at the same project location; defaults to adjacent morphir.toml
--dry-runReturn mappings, edits, conflicts, and verification without writing
--jsonEmit a structured migration plan/result and diagnostics

Output selection must preserve the project location and relative-path meaning. An output that introduces same-format primary ambiguity is refused.

Migration behavior

Map name, sourceDirectory, and exposedModules to their canonical project fields. Preserve supported additional settings and materialize legacy-derived defaults, including Elm frontend and checkpoint behavior, so the new TOML works without the classic source.

If a TOML target exists, add only nonconflicting missing values while preserving comments, formatting, and unrelated settings. Identical existing values are unchanged. Different values or same-file legacy/canonical conflicts block the plan. Every source setting must be mapped, preserved with equivalent meaning, or reported as blocking. Unsupported dependency or decoration shapes cannot be silently dropped.

Before application, compare project-scoped resolution before migration with the candidate TOML while excluding the classic source from the candidate. Exclude ambient machine, personal, environment, CLI, and activated-profile overlays from both sides so they cannot hide a mismatch. Retain the declared base workspace context and compare resolved path meaning as well as values. Report the expected JSON-to-TOML provenance changes separately.

Only a verified plan can be applied. Recheck source, target, and relevant workspace preconditions before atomic target replacement. Keep morphir.json byte-identical for older consumers; removal is a separate deliberate action after those consumers have moved. Repeating a completed migration is a no-op.

Results and exit codes

The result identifies source/target paths, mappings, planned or applied edits, blockers, equivalence status, and expected origin changes. Sensitive values are redacted. Stdout contains results only; diagnostics go to stderr. JSON mode includes structured diagnostics in the result.

CodeMeaning
0Verified success, applicable preview, or unchanged repeat
2Invalid input, unsupported field, conflict, failed equivalence check, I/O failure, or recovery required

Validation and stale-plan failures leave source and target unchanged. If a write failure cannot be fully recovered, report the affected paths and retained recovery data rather than claiming success or discarding evidence.

5 - project migrate

Preview and apply a loss-checked migration from classic morphir.json to TOML

project migrate

morphir project migrate [path] [--output path] [--dry-run] [--json] [--discovery-boundary directory]

Migrate one classic project configuration to TOML. The command maps classic fields, checks that every original value is accounted for, and verifies the candidate independently of the classic manifest. It keeps morphir.json byte-identical for older consumers.

path accepts one project directory or classic morphir.json. With no path, shared discovery selects the current unambiguous project. Invoking from a workspace member selects that member. An unlisted project directory is refused; migration never substitutes a sibling or a containing root project for it. A virtual workspace with no unambiguous project requires a member directory or manifest path.

Options

OptionMeaning
--output pathChoose a supported TOML primary layout at the same project location
--dry-runReturn the complete plan without creating or changing files
--jsonEmit one structured JSON result on stdout
--discovery-boundary directoryBound discovery to this directory tree. Default: MORPHIR_DISCOVERY_BOUNDARY, else the filesystem root. The current directory and path must lie inside it; otherwise the command fails with exit code 2 and a discovery.boundary diagnostic before reading anything

Migration always excludes machine and personal configuration, so MORPHIR_ISOLATED does not apply. See containment from the environment.

The default output is the corresponding TOML primary adjacent to the selected classic manifest: morphir.json becomes morphir.toml; a dot-config config.json becomes config.toml. Supported primary layouts are morphir.toml, .morphir/morphir.toml, and .config/morphir/config.toml within the same project. Hidden and dot-config layouts retain the project’s location as the anchor for relative source paths. An arbitrary TOML filename, another project directory, a lower-format output, or an output that creates primary ambiguity blocks migration.

Before and after

A classic manifest:

{
  "name": "Orders",
  "sourceDirectory": "elm-src",
  "exposedModules": ["Order"]
}

The equivalent TOML includes the implicit legacy defaults:

[project]
name = "Orders"
source_directory = "elm-src"
exposed_modules = ["Order"]

[frontend]
language = "elm"

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

Migration does not add an unrelated ir.format_version value. Supported extra settings survive conversion. A raw field ledger records mapped, retained, derived and blocking values. Unsupported nonempty dependency/decorations shapes, nulls, values TOML cannot represent, and same-file classic/canonical conflicts block application. Resolve the reported fields deliberately before retrying. There is no drop-unsupported or overwrite switch.

Existing equal TOML entries keep their original spelling and comments. Missing values are added with the source-preserving editor. Different values block the entire write. A successful repeated migration returns unchanged and preserves the target timestamp.

Verification

The comparison resolves project-scoped typed settings and resolved path meaning before and after migration. It excludes the converted classic source from the candidate and excludes machine, personal, environment, explicit CLI and activated profile overlays from both sides. Declared base workspace settings remain. Expected origin changes are reported separately from value differences.

This means an ambient override cannot hide a bad conversion, and the retained classic file cannot supply a value omitted from TOML. Profile declarations and other supported source settings remain in the field ledger and conversion.

Examples

# Review the current project without writes.
morphir project migrate --dry-run

# Preview one member in JSON for CI review.
morphir project migrate packages/orders/morphir.json --dry-run --json > migration.json

# Use a dot-config primary at the same project location.
morphir project migrate packages/orders \
  --output packages/orders/.config/morphir/config.toml

# Apply the default adjacent target, then verify the repeat is unchanged.
morphir project migrate packages/orders
morphir project migrate packages/orders --json

Relative output arguments resolve from the invocation directory. This command adopts configuration only. It performs no source-code conversion, IR conversion, IR-version upgrade, sibling batch operation or workspace-membership edit.

Results and exit codes

ExitMeaning
0Verified application, applicable preview, or unchanged repeat
2Invalid input, loss, conflict, failed verification, stale evidence or write/recovery failure

Human output lists the outcome, source/target, field mappings, proposed edits and verification. JSON follows the project migration result schema. The report includes redacted values, loss ledger, planned edits, independent verification, origin changes, structured diagnostics and retained recovery paths. Diagnostics go to stderr; JSON stdout contains one valid document, including argument and host-initialization failures. Secret references are never executed.

Troubleshooting and recovery

  • For an IR-file rejection, pass the project directory or morphir.json.
  • For an unmapped field or unrepresentable value, inspect its raw path in the ledger and make a deliberate supported equivalent before retrying.
  • For an existing-target conflict, reconcile the reported values manually. Migration will not overwrite a differing TOML value.
  • For ambiguity, retain one primary per format group across supported layouts.
  • For a stale plan, preview again. Source, target and relevant workspace evidence are checked again at application time.
  • For rolled-back, the transaction restored the original target. Inspect the diagnostic, correct the write failure and plan again.
  • For recovery-required, preserve the reported journal, stage and backup paths. Inspect their transaction ownership before any manual recovery. Do not remove the original classic file or overwrite later user edits.

Writes reuse the configuration editor’s staged per-file atomic replacement and explicit recovery reporting. They do not claim multi-file crash atomicity. Rollback does not automatically delete a subsequently edited TOML. Removal of a classic manifest is a separate user action after older consumers have moved.

See configuration editing for transaction and host boundaries, and the configuration guide for discovery and layering.

6 - Workspace migration

Bounded adoption of classic projects into a Morphir workspace

Workspace migration

morphir workspace migrate coordinates the shared project migrator, bounded inventory, aggregate verification, and recoverable application. Other hosts use the same library services. The complete command reference describes the JSON contract and recovery outcomes.

Use it when a repository contains several classic projects, including when the repository does not yet declare a Morphir workspace. For one project without workspace membership changes, use morphir project migrate.

Synopsis

morphir workspace migrate [<directory>] [--include <pattern>] [--exclude <pattern>] [--dry-run] [--json]

The supplied directory, or the current directory when omitted, bounds the inventory and resulting edits. The command does not walk ancestors to find more projects to adopt.

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

Options

OptionPurpose
<directory>Root of the bounded inventory; defaults to the current directory
--include <pattern>Refine candidates using a root-relative pattern; repeatable
--exclude <pattern>Exclude candidate paths using a root-relative pattern; repeatable
--dry-runPreview the complete workspace and project plan without writing
--jsonEmit a structured plan/result and diagnostics

Inventory stays within the selected root, skips discovered links, stops at nested declared workspaces, and excludes version-control, dependency, and build-output directories. Existing workspace exclusions remain effective.

Membership and project plans

The preview lists candidates, exclusions, nested boundaries, already migrated members, project edits, workspace edits, blockers, and verification results in deterministic path order.

Propose explicit root-relative member paths while preserving existing membership, settings, and a valid default member. Already migrated members remain members without being rewritten. A root project retains its identity; a root without a project is a virtual workspace. Duplicate project names are diagnosed without merging their distinct paths.

Reuse the single-project migration contract for every selected classic project. One blocked project blocks the batch. Verify each classic-to-TOML conversion in the same proposed workspace context, excluding the converted classic source from the candidate and ambient overlays from both sides. Preserve source-path meaning and project settings independently of serialization.

The plan separately reports intended workspace adoption effects: the new topology, member/default selection, and shared output root. For example, standalone defaults under each member’s .morphir/out can become <workspace>/.morphir/out. This declared adoption effect is not a conversion mismatch. Owning workspace context supplies topology and the output root; members do not inherit the root’s entire project or workspace document subtrees. No existing generated files are moved by migration.

Check the resulting workspace as a whole against those declared effects. Do not relax loss checks or rely on classic manifests or ambient overrides to hide an unexplained setting change.

Application and recovery

Validate and stage the entire plan before replacing any destination. Recheck relevant source and target preconditions, coordinate overlapping writers, and journal replacements so a commit failure can attempt rollback.

Multiple file replacements are not crash-atomic. Report restored and unresolved paths explicitly if rollback fails. Retain recovery data and block overlapping writes until recovery is resolved. A preview encountering pending recovery reports recovery-required with exit 2 and the retained metadata without modifying it. Successful application and complete rollback clean their temporary artifacts. Every classic morphir.json remains unchanged.

An equivalent repeat reports unchanged files without rewriting them. A valid existing workspace needing no further conversion is a successful no-op. An undeclared root with no eligible projects reports that no adoption can be planned.

Results and exit codes

Stdout contains results only; diagnostics go to stderr. JSON results include structured diagnostics and per-project outcomes, with sensitive values redacted.

CodeMeaning
0Verified success, applicable preview, or unchanged existing workspace
2Invalid or blocked plan, failed verification/application, or recovery required

7 - workspace migrate command

Preview and apply bounded workspace adoption with per-project loss checks and recoverable writes

workspace migrate

morphir workspace migrate adopts eligible projects within one selected directory. It uses the reusable inventory, aggregate planner/verifier, and transaction APIs. The same operation is available to embedded hosts without invoking this command.

Synopsis

morphir workspace migrate [<directory>]
    [--include <pattern>]... [--exclude <pattern>]...
    [--dry-run] [--json] [--discovery-boundary <directory>]

<directory> defaults to the current directory. It must name an existing directory. That directory bounds inventory, discovery, and all proposed destinations, including when it contains no workspace declaration. Migration does not search ancestors for projects to adopt. Select the intended repository root explicitly when invoking from a member or source directory.

Workspace migration never reads machine or personal configuration, so MORPHIR_ISOLATED and --isolated do not apply to it. See containment from the environment.

Arguments and options

ControlMeaning
<directory>Bounded root; omitted means current directory
--include <pattern>Include candidate project directories matching a root-relative pattern; repeatable
--exclude <pattern>Exclude candidate project directories; repeatable
--dry-runReturn the complete verified plan and write nothing
--jsonWrite one redacted JSON result to stdout; diagnostics remain on stderr
--discovery-boundary <directory>Outer limit the directory must lie within. Default: MORPHIR_DISCOVERY_BOUNDARY, else the filesystem root. A directory outside it fails with exit code 2 and a discovery.boundary diagnostic, and nothing is written
--helpShow usage without discovery or migration

Patterns are relative to the selected root. * and ? match within a path segment; ** matches across directory segments. Absolute patterns and .. segments are invalid. Exclusions prune matching subtrees. Shell-quote wildcards so the shell does not expand them before the CLI receives them. Prefix a literal leading-dash pattern with ./, for example --include './--experimental', so it cannot be mistaken for a missing value followed by an option. Existing workspace.exclude settings remain effective alongside the supplied exclusions. Explicit filters do not override nested workspace, link, version-control, dependency, or build-output boundaries. Invalid controls block before application.

morphir workspace migrate . --dry-run
morphir workspace migrate ./repo --dry-run --json
morphir workspace migrate . --include 'packages/*' --include 'services/*' \
  --exclude 'packages/experimental-*' --exclude 'services/retired' --dry-run --json

Inventory and membership

Inventory reports project candidates, excluded paths, skipped links, nested declared workspace boundaries, and already migrated projects in deterministic path order. It remains bounded even without an existing workspace. It skips generated output, version-control, and dependency directories and does not follow discovered links.

The plan uses explicit root-relative paths for new members. Existing membership, exclusions, settings, and valid workspace.default_member remain in place. Already migrated members are retained without rewriting their primaries. A root project keeps its identity. A root without a project becomes a virtual workspace. Duplicate project names produce diagnostics while their distinct paths remain separate.

For example, a new virtual workspace containing packages/orders and packages/payments receives:

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

Each classic project receives its own supported TOML primary. Hidden primary layouts keep the owning project directory as the source-path anchor.

Conversion and verification

Every selected classic project uses the single-project migration contract, including the complete raw loss ledger, unsupported-field blockers, conflict checks, and materialized legacy defaults. A blocked member blocks the aggregate before any destination replacement.

Migration compares each classic source and its TOML conversion inside the same proposed base workspace context. It excludes converted classic sources from the candidate and ambient personal, machine, environment, explicit CLI, and activated profile overlays from both sides. Declared profile controls are still checked. The comparison preserves typed values and source-path meaning and checks the resulting workspace discovery, membership, and selection.

Adoption effects appear separately in the plan: ownership, membership, default selection, and shared output roots. Standalone projects may acquire a shared <workspace>/.morphir/out output context. That effect cannot excuse unrelated setting or source-path changes. Migration moves no generated artifacts and keeps every classic source byte-identical.

Output

Human output names the outcome, inventory entries, member paths, project edits, workspace edits, adoption effects, and verification. Diagnostics go to stderr.

JSON follows the workspace migration result schema. Its fields include:

FieldContent
operation, outcome, exitCodeOperation identity, result state, and exit code
rootPath, rootIsProjectBounded root and root versus virtual identity
changed, dryRun, verifiedPlanned changes, preview mode, and aggregate verification
inventoryPath, relative path, kind, reason, and project name
projectsPer-project redacted ledger, edits, conflicts, and independent verification
members, workspaceTarget, workspaceEditsReviewed membership and workspace primary edits
adoptionEffectsIntentional ownership, membership, default-selection, and output-root effects
diagnosticsStructured warnings and blockers also sent to stderr
recoveryArtifacts, restoredPaths, unresolvedPathsRetained owned evidence and recovery state
uncertainDirectories, preservedDirectoriesDirectory paths whose ownership is uncertain, or that explicit recovery was instructed to preserve
valueTypesTyped encoding metadata for nonfinite and temporal values in workspace edits

Each nested project report has its own valueTypes array. Its JSON pointers are relative to that project report. Sensitive settings and opaque secret references are redacted throughout the reports; reference targets are never resolved.

morphir workspace migrate . --dry-run --json > workspace-preview.json
morphir workspace migrate . --json > workspace-result.json
morphir workspace migrate . --json # unchanged after successful adoption

Exit codes

CodeMeaning
0Applicable preview, successful application, or valid existing-workspace no-op
2Invalid input, blocked conversion, failed verification, stale evidence, failed application, or recovery required

An undeclared directory with no eligible projects reports no adoption plan and exits 2. A valid existing workspace requiring no conversions succeeds unchanged. Equivalent repeats preserve target bytes and timestamps.

Application and recovery

Application validates and stages the entire aggregate before any destination is replaced. It coordinates overlapping cooperating writers, rechecks source, target, listing, and topology preconditions, and records durable recovery evidence. A later replacement failure triggers rollback attempts. The result identifies restored and unresolved paths.

Per-file atomic replacement does not make the batch crash-atomic. Incomplete rollback or interrupted state retains journals, stages, and backups and produces recovery-required. A read-only preview also reports pending recovery with exit 2 and retains the journal metadata. Overlapping configuration editing and project/workspace migration writes remain blocked until explicit recovery resolves that state. Successful application and proven complete rollback remove only owned transaction artifacts. User changes and unresolved evidence are preserved. If interruption occurs after directory creation but before its ownership receipt returns, uncertainDirectories names those paths explicitly. Recovery does not claim or delete them. The shared recovery API can explicitly preserve reviewed uncertain directories and reports them in preservedDirectories.

Use the shared inspection and recovery APIs. There is no automatic cleanup of uncertain recovery state. Do not delete a journal or backup to force a repeat migration through. Classic sources are never removed by migration or recovery.

Troubleshooting and automation

For a blocker, inspect projects[*].ledger, projects[*].conflicts, and diagnostics before changing source or target settings. A fresh preview is needed after source, target, relevant listing, or topology changes. A shared-output-root adoption effect does not permit a different source directory.

CI should inspect the exit code and retain a failed JSON result as an artifact:

morphir workspace migrate "$REPOSITORY" --dry-run --json > workspace-plan.json

A preview still requires independent review before choosing to apply it. No release, deployment, source deletion, or generated-output publication is part of this operation. Migration is owned by project and workspace; there is no config migrate alias.

See the walkthrough, workspace overview, and troubleshooting.

8 - mck

Run the separately installed Morphir Compatibility Kit, morphir-mck

mck

morphir mck <args> runs morphir-mck <args>, the Morphir Compatibility Kit (MCK), the way dotnet morphir runs dotnet-morphir. The kit is a development tool from finos/morphir, installed separately. This CLI does not include it and installs nothing: without morphir-mck, morphir mck only says where it looked.

The commands and options of the kit are those of morphir-mck. See its own help with morphir mck --help or morphir-mck --help.

Synopsis

morphir mck [<argument>...]
dotnet morphir mck [<argument>...]

Every entry point routes mck the same way: the Native AOT morphir, the managed morphir and dotnet-morphir. This pass-through uses MorphirApplication.Run. A custom host that parses through CreateApp first gets System.CommandLine’s help and response-file expansion before the routing action.

What reaches the tool

  • Arguments. Every argument after mck, in order and unchanged. The CLI does not read them, so --help, --version and @file go to the tool.
  • Working directory and environment. The CLI’s own.
  • Standard input, output and error. The CLI’s own handles, so pipes, redirection and terminal colours behave as for morphir-mck run directly. The CLI writes nothing to standard output when it routes.
  • Exit code. morphir mck exits with the exit code of morphir-mck.

How morphir-mck is found

  1. If MORPHIR_MCK is set and not empty, it must be the absolute path of the tool. A relative path is refused, so the result never depends on the current directory. On Windows it must be an .exe file. On Linux and macOS it must have an execute permission.
  2. Otherwise the folders of PATH are searched in order. Only absolute folders count: empty and relative entries, . included, are skipped. The first folder that holds morphir-mck.exe (Windows) or an executable morphir-mck (Linux and macOS) wins.

The current directory and the folder of morphir itself are never searched. On Windows, scripts such as morphir-mck.cmd are not candidates, because they would run through a shell.

When the tool is missing

$ morphir mck run
morphir mck runs the Morphir Compatibility Kit, morphir-mck, which is installed separately: it is a development
tool from finos/morphir, and this CLI does not include it. MORPHIR_MCK is not set, and no absolute folder on PATH
holds morphir-mck. ...
$ echo $?
127

The message goes to standard error, and standard output stays empty.

Interrupts and termination

  • Ctrl+C reaches morphir-mck directly. The tool decides how to stop, and morphir mck waits for it and exits with its code.
  • Windows. Before it starts the tool, morphir joins a job object that ends every process in it when morphir ends. If morphir is killed, for example from Task Manager or by a CI runner, the tool and every process it started stop too. This also stops a process that the tool meant to leave running after it exits. If the job cannot be joined, the CLI warns on standard error and continues without that kill-on-close protection.
  • Linux and macOS. SIGTERM or SIGHUP to morphir stops the tool’s process tree, and morphir exits with 143. SIGKILL to morphir cannot be passed on, so the tool keeps running in that case.

Exit codes

CodeMeaning
The tool’s codemorphir-mck ran.
126The resolved file could not be started.
127No tool to run: none found, or MORPHIR_MCK was refused.
143Linux and macOS: morphir received SIGTERM or SIGHUP and stopped the tool.

Only mck is routed

mck is the only command that runs another executable. Any other unknown command, such as morphir lint, is a command-line error, even when an executable morphir-lint is on PATH.

9 - morphir extension runtimes

Report whether this host can run WASM extensions, with its engine, profiles and bounds, or the reason it cannot

morphir extension runtimes

Reports the extension runtimes of the host you run. Today there is one, wasm: the runtime that runs WASM extension modules. The report says whether it is available in this executable, which engine runs the modules, which profiles the host runs and which bounds it enforces. When the runtime is unavailable, the report gives the reason.

Synopsis

morphir extension runtimes [--json]
dotnet-morphir extension runtimes [--json]

Options:

  • --json: Write one JSON document on stdout.

Description

The command loads no module. It asks the executable you run, not its file name or its install folder, so a copy of morphir that lost its engine library reports that copy’s state.

The host checks the engine only when something needs it: this command, or a command that runs a WASM module. It checks once per process. Starting morphir, --version, --help, extension list with only built-in extensions, and every other command that needs no WASM never load the engine. A missing or broken engine therefore cannot break them.

The command exits with 0 whether the runtime is available or not: the report is the result. Logs and diagnostics go to stderr, so morphir extension runtimes --json | jq works.

Output

An available runtime:

$ morphir extension runtimes
Host: morphir
Platform: win-arm64
wasm: available
  Engine: wasmtime 50.0.1
  Profiles: extism-kernel-v1
  Bounds: memory ceiling 268435456 bytes (256 MiB), call deadline enforced, instruction budget enforced
{
  "host": "morphir",
  "platform": "win-arm64",
  "runtimes": [
    {
      "kind": "wasm",
      "available": true,
      "engine": { "name": "wasmtime", "version": "50.0.1" },
      "profiles": ["extism-kernel-v1"],
      "bounds": { "memoryBytes": 268435456, "deadline": "enforced", "budget": "enforced" }
    }
  ]
}

An unavailable runtime, here a dotnet-morphir whose engine library is not in its folder:

$ dotnet-morphir extension runtimes
Host: dotnet-morphir
Platform: win-arm64
wasm: unavailable (engine_missing)
  Engine: wasmtime 50.0.1
  Reason: the engine's native library is not in this installation
{
  "host": "dotnet-morphir",
  "platform": "win-arm64",
  "runtimes": [
    {
      "kind": "wasm",
      "available": false,
      "reason": "engine_missing",
      "engine": { "name": "wasmtime", "version": "50.0.1" }
    }
  ]
}
FieldMeaning
hostThe command that wrote the report: morphir or dotnet-morphir. The Native AOT morphir and the managed morphir both report morphir.
platformThe platform the process runs on, named as the engine package names its native folders: win-x64, win-arm64, linux-x64, linux-arm64, osx-x64, osx-arm64, or another name such as linux-musl-x64 for a platform without an engine.
runtimes[].kindwasm.
runtimes[].availabletrue when this executable can run a WASM module.
runtimes[].reasonOnly when available is false: why. See Reasons.
runtimes[].engineThe engine this build carries, by name and version. It is the version of the Wasmtime package the build references, also when the engine cannot load.
runtimes[].profilesOnly when available: the WASM execution profiles the host runs. This host runs extism-kernel-v1 only.
runtimes[].boundsOnly when available: memoryBytes is the ceiling of each linear memory of a session, and deadline and budget say that the call deadline and the instruction budget are enforced. The values are the defaults of the pinned WASM execution profile contract.

The report has no member for the deployment offering (native-aot, self-contained, framework-dependent or dotnet-tool). The host identity now records named release profiles, but this report has not yet added those fields. It does not guess a deployment from the file name or path. A runner that needs the offering is given it as an input.

Reasons

ReasonWhat it meansWhat you can do
platform_unsupportedThe engine has no build for this platform, for example Linux with musl (Alpine).Use a supported platform for WASM extensions. Every other command works here.
engine_missingThe engine’s native library (wasmtime.dll, libwasmtime.so or libwasmtime.dylib) is not where the host looks for it. A copy, an archive or a package lost the file.Reinstall or extract the release again, keeping every file beside the executable, including the runtimes folder of the dotnet tool.
engine_load_failedThe library is there but did not load: it is for another architecture, it is damaged or of another version, or one of its own dependencies is missing.Reinstall the release for your platform and architecture. If that does not help, report it with the output of this command.
win_x64_trap_unsupportedWASM execution on Windows x64 is unavailable because trap handling is not compatible with the required process protections. The host refuses before loading the engine or reading a module.Use another supported platform for WASM. Other CLI commands remain available. The protection-preserving fix is tracked in #609.

No configuration or environment setting enables WASM on Windows x64. CET, context validation, signal-based traps and Spectre mitigations retain their defaults. No other command loads the engine.

The host decides the reason the first time something needs the engine: this report, loading a module, or listing a WASM extension. --version, --help and the worker’s argument and build-identity checks never load it.

Commands that need the runtime

A command that needs the WASM runtime in a host without it fails with extension.runtime_unavailable before it reads the module, and names the host, the platform and the reason. It never shows a native library loading error. Today the only such command is the internal morphir extension worker. It checks its arguments, then the build identity, then the engine, then the module, and reports the first that fails. Without a usable engine it writes one refused event with the code extension.runtime_unavailable on stderr, nothing on stdout, and exits with 3:

{"profile":"morphir/command-event","contractVersion":"0.1.0","command":"extension worker","sequence":1,"event":"refused","error":{"code":"extension.runtime_unavailable","message":"The WASM module of extension 'python' at '/work/python.wasm' cannot be loaded with extension.runtime_unavailable: the WASM runtime of dotnet-morphir 0.4.0 on linux-musl-x64 is unavailable (platform_unsupported): the engine has no build for this platform."}}

In extension list, a wasm extension has available set to false in a host whose runtime is unavailable, and built-in extensions stay available. See extension list.

See also

10 - morphir mck wasm-host (experimental)

Experimental and internal: run the crafted WASM host cases against a host’s worker mode, and check the report

morphir mck wasm-host

Development runner adapter reference. Product morphir mck ... forwards every argument to the separately installed morphir-mck, as described in morphir mck. The product contains no compatibility runner. The commands below document the retained adapter source awaiting migration to the dedicated development entry point. They do not establish that the installed tool supports these subcommands. Reports contain observations only; qualification awaits parent task 1.4. Contributors: see WASM host qualification.

Synopsis

morphir mck wasm-host run --kit <dir> --host <executable> --report <file>
    --offering <name> --platform <rid> --engine <text>
    [--host-arg <arg>]... [--host-profile <profile>]...
    [--deviations <file>]
    [--guest-kit-report <file> --guest-kit-host-sha256 <hex> --guest-kit-module-sha256 <hex>]

morphir mck wasm-host report-check --kit <dir> --report <file>
    [--expect-offering <name>] [--expect-platform <rid>]

Description

run reads the wasm-host suite (conformance/suites/wasm-host) and the pinned corpus it is generated from, and runs every case through the host’s worker mode:

<host> [--host-arg ...] extension worker --abi <abi> --module <wasm> --build-identity <id> [--request-bytes <n>] [--response-bytes <n>] [--instructions <n>] --json

The limits and the instruction budget are passed only for a case that sets them (worker.requestBytes, worker.responseBytes, worker.instructions); otherwise the worker keeps the contract’s defaults. It reads the build identity once from <host> extension worker --help --json. Before a case runs, it compares the module’s SHA-256 with the digest the corpus records, and refuses the case when they differ. It writes the case’s frames on stdin, reads the stdout frames and the stderr events, and compares them with the case’s expectation. A worker still alive one call deadline per frame plus the corpus margin (2 seconds) after its last frame has its process tree stopped. The runner knows nothing about the host except its path and arguments.

report-check reads a report back, checks it against the same kit and recomputes every outcome from the recorded observations. It never trusts a stored outcome.

Neither command computes a qualification verdict: the verdict belongs to parent task 1.4, which is not done. A report holds per-case observations and outcomes, and says so in its qualificationNotice.

Options of run

OptionMeaning
--kitThe suite directory, for example conformance/suites/wasm-host. The pinned corpus is found in the checkout that holds it.
--hostThe host executable.
--host-argAn argument before extension worker, repeatable. For example --host dotnet --host-arg exec --host-arg worker.dll.
--host-profileA profile the host declares it runs, repeatable. extism-kernel-v1 is always run; give morphir-bytes-v1 for a host that runs the byte profile. A case for a profile the host does not declare is not_applicable.
--offering, --platform, --engineRequired. What the host declares about itself, for example native-aot, linux-x64 and wasmtime 50.0.1. A worker reports none of them, so the runner is given them, and the report records them (corpus convention identity). The platform is the host executable’s, which can differ from the machine’s, as for an x64 build under emulation. Without one of them run exits with 2 before any case.
--reportThe report file to write.
--deviationsA JSON file {"deviations": [{"caseId", "tracking", "note"}]}. A deviation is attached to its case in the report; it never changes an outcome.
--guest-kit-report and its two digestsA passing generic guest kit run through the same executable, cited by the report. The cited host digest must be the --host digest.

Outcomes

OutcomeMeaning
passedEvery run satisfied every matcher of its expectation. For a run with alternatives, one alternative whose condition matches the host is enough, and the report names it in satisfied: the run, the alternative’s number counted from 1 in corpus order, and its condition. For a runaway case this says which bound ended the call.
failedA run satisfied no expectation that applies to the host. The report names, for each failure, the run, the alternative (null for a run without alternatives), the matcher and why; a run with alternatives lists the failures of every applicable alternative.
errorThe runner could not judge the case, for example because no started event declared a bound the case depends on. Never a pass.
not_applicableThe case needs a profile the host does not declare.
not_enforcedThe host’s started events declare a bound of the case not enforced.
refusedThe module’s bytes differ from the recorded digest; no worker started.

Exit codes

Coderunreport-check
0Every applicable case passed. A not_enforced case passes only for the instruction budget; the memory ceiling and the call deadline are required.The report is the kit’s and every recomputed outcome passes.
1A case did not pass, or an operational error, such as a host that cannot give its build identity.The report is invalid, or a recomputed outcome does not pass.
2An invalid invocation: a missing option, the offering, platform and engine among them, a host that is not a file, an unknown profile, or a kit of another version.An invalid invocation: a missing option or report, or a kit of another version.

Output

stdout carries one JSON document: the command, "experimental": true, the exit code, the report path and digest, and the totals by outcome. Progress and diagnostics go to stderr.

{"command":"mck wasm-host run","experimental":true,"exitCode":1,"report":"/work/.dev/wasm-host.json","reportSha256":"…","host":"/opt/morphir/bin/morphir","hostSha256":"…","totals":{"cases":59,"passed":57,"failed":2,"error":0,"refused":0,"notApplicable":0,"notEnforced":0},"qualificationNotice":"…"}

The report (morphir/mck-wasm-host-report 0.1.0-draft.2) is indented JSON with a fixed member order, so the same observations give the same bytes. It records the suite and corpus pin, the contract version, the runner, the host executable’s path, arguments and SHA-256, its build identity, profiles, offering, platform and engine, what the started events declared about each bound, and for each case its module digests, outcome, the alternatives it satisfied or its failures, and the observation of each run: the worker arguments, stdout (base64), every stderr line with the time it was read, the exit status and the times. report-check refuses a report that does not identify the host’s offering, platform and engine.

Examples

# The MoonBit native CLI, which also runs the byte profile.
morphir mck wasm-host run --kit conformance/suites/wasm-host \
  --host /path/to/morphir --host-profile morphir-bytes-v1 \
  --offering native-cli --platform win-x64 --engine "moonrockz/extism 0.3.0" \
  --report .dev/wasm-host.json

morphir mck wasm-host report-check --kit conformance/suites/wasm-host --report .dev/wasm-host.json

# A .NET executable, here the Native AOT morphir. It runs extism-kernel-v1 only, so no --host-profile.
morphir mck wasm-host run --kit conformance/suites/wasm-host \
  --host artifacts/executables/win-arm64/morphir.exe \
  --offering native-aot --platform win-arm64 --engine "wasmtime 50.0.1" \
  --report .dev/wasm-host-native-aot.json
  • conformance/README.md: the suite and its generator.
  • tests/Fixtures/MepWasm/: the pinned contract and corpus.

11 - morphirup

Set up the Morphir toolchain with morphirup and run it, then the command reference

morphirup

morphirup installs the Morphir command line, morphir, with the F# frontend and backend it uses, and runs them. It is one executable and needs no .NET. For a walkthrough with real output, see Your first model with morphirup.

Acquire the Morphir CLI and set up a toolchain

PlatformInstall morphirupsetup, show, select and run
Windows x64 and ARM64YesYes
macOS on Apple siliconYesYes
Linux x64 and ARM64YesNo: setup doesn’t support Linux yet

Intel Macs are not supported.

  1. Install morphirup, as Installation shows.

  2. In your project folder, see what setup would install, then install it:

    morphirup setup --dry-run
    morphirup setup --yes
    

    The dry run checks the release and installs nothing. setup --yes downloads the Morphir CLI, the F# frontend and the F# backend from the release of the same version as morphirup, checks them against the release’s checksums, tries them out, and selects them. This is the only step that needs the network.

  3. See what is installed:

    morphirup show
    

    "status": "shown" means a toolchain is set up and selected.

  4. Switch to another installed toolchain when you need to:

    morphirup select --generation <generation-sha256> --dry-run
    morphirup select --generation <generation-sha256>
    

    Each setup --yes installs a new copy, called a generation, and selects it. Older generations stay installed. setup and show print the generationSha256 of the one they set up or show. select checks every file of the generation you name, then selects it, for example to go back after a newer setup. use is the same command.

  5. Run the CLI. Everything after -- goes to morphir:

    morphirup run -- compile
    morphirup run -- generate --target fsharp --output generated
    

    run uses only what is installed, so it works offline.

morphirup keeps everything in one folder, the Home: MORPHIR_HOME if it is set, and .morphir in your user folder otherwise. setup also takes --home <dir>, and show, select and run take --home <absolute path>.

When something goes wrong, see Troubleshooting.

How release setup works

morphirup is the standalone Morphir toolchain manager of the approved change add-deployment-and-acquisition (finos/morphir, decisions UP-1, UP-2 and LOCK-1). It is a Native AOT executable. Release setup is experimental. Historical recipe setup remains plan-only. Explicit local host installation, selection and execution remain available through the local candidate instructions. Publisher authentication, portable lock restore, proxy installation and shell setup remain unfinished.

Setup uses its own release version unless .morphir/toolchain.toml requests an exact version. It downloads the release’s SHA256SUMS and the bundle file of its own platform (morphir-toolchain-<rid>.json), then the runtime-bundled Morphir host and independent F# frontend and backend that the bundle names. It checks their byte counts and SHA-256 values, installs their complete folders, executes the fixed readiness workflow, and selects the completed generation last. --home is optional and defaults to MORPHIR_HOME, then ~/.morphir. A dry run reads only SHA256SUMS and the bundle, and creates no Home, locks, staging, receipts or selection and runs no guest. Setup refuses before those effects if the final host or provider executable path would exceed the Windows launch limit of 259 characters. Choose a shorter --home when the diagnostic names this limit.

Setup normally removes its own private staging folders. If the mutation lease stays busy for five seconds, cleanup leaves that folder and writes one warning on stderr without changing setup’s achieved result. A later setup does not sweep deferred or interrupted stages; that follow-up is tracked in #617.

This is release-checksum delivery, not publisher-authenticated. HTTPS and a matching release checksum do not authenticate the publisher. The release transport starts only at the exact finos/morphir-dotnet repository and version tag. It permits at most three HTTPS redirects to github.com, release-assets.githubusercontent.com or objects.githubusercontent.com, without credentials or a downgrade. The generic acquisition transport keeps its existing no-redirect policy.

For composed generations, run selects the installed F# frontend when compile --language fsharp is explicit, and the installed backend for generate --target fsharp. Without --language, the host resolves project configuration; the manager does not force F# over another language. Explicit --extension or --extension-id selections are passed through unchanged.

After setup succeeds, the same run commands use installed folders and retained selected records without network access or the download cache. The existing explicit local candidate commands remain diagnostic entry points. This scope does not implement publisher authentication, trust commands, moving release channels or portable lock restore. The manual Windows released morphirup journey workflow validates the actual published executable after assets are available.

Each generation’s receipt, toolchains/generations/<generation-sha256>/receipt.json in the Home, names the host’s implementation, deployment, platform, version and entry point. toolchains/default.json records the selected generation, and catalog/extensions.json lists the installed providers. select, show and run read only the current local-toolchains family; in a Home set up by an earlier family they refuse with local.setup_required, and setup --yes installs the current family beside the earlier generations.

On macOS

On macOS on Apple silicon, the installed host and providers are the osx-arm64 builds, with the entry points morphir, morphir-fsharp-frontend and morphir-fsharp-backend. Intel macOS (osx-x64) is not supported: an x64 manager under Rosetta refuses setup and run. The executables are not notarized.

  • Setup keeps the bytes of each executable as the SDK published them. The SDK signs each apphost ad hoc, and ARM64 macOS runs only a signed executable, so the signature stays valid.
  • A host file is installed with mode 0755 when its archive mode is executable, as for the entry point and createdump, and 0644 otherwise. A provider file is installed read-only, 0555 or 0444. Setuid, setgid and group or other write bits in an archive are never installed.
  • Setup uses the physical path of the parent folders of Home. On macOS, $TMPDIR is below the link /var, and the installed host refuses a source file below a link. A Home that is itself a link is refused.
  • run starts the host with the same environment as on Windows: MORPHIR_HOME, MORPHIR_ISOLATED=1, the discovery boundary and DOTNET_MULTILEVEL_LOOKUP=0, and no DOTNET_ROOT, DOTNET_ROOT_X64, DOTNET_ROOT_ARM64, DOTNET_SHARED_STORE, DOTNET_ADDITIONAL_DEPS or DOTNET_STARTUP_HOOKS.
  • Process ownership is weaker than on Windows. On Windows the host runs in a kill-on-close job: when the host exits, morphirup stops each process that is still in the job, and when morphirup dies, the job stops them all. On macOS the host has no process group of its own. When you cancel run (Ctrl+C) or its output fails, morphirup stops the host and the processes below it that are still its descendants at that time. It does not stop a process that the host started and left running when the host exited normally, and it stops nothing when morphirup itself is killed.

To build and install the macOS ARM64 inputs from a checkout, see macOS ARM64 local candidate.

Download and verify

Releases of morphir-dotnet attach morphirup for five platforms, beside the morphir assets on the releases page:

PlatformExecutableArchive
Linux glibc x64morphirup-linux-x64morphirup-linux-x64-v<version>.tar.gz
Linux glibc ARM64morphirup-linux-arm64morphirup-linux-arm64-v<version>.tar.gz
Windows x64morphirup-win-x64.exemorphirup-win-x64-v<version>.tar.gz
Windows ARM64morphirup-win-arm64.exemorphirup-win-arm64-v<version>.tar.gz
macOS ARM64morphirup-osx-arm64morphirup-osx-arm64-v<version>.tar.gz

A Linux executable needs glibc 2.35 or later from the machine, which Ubuntu 22.04, Debian 12 and later releases of both have, and nothing else: no .NET, no ICU and no OpenSSL. It does not run on musl distributions such as Alpine. The macOS executable needs macOS 12.0 or later: that is the minimum its LC_BUILD_VERSION load command declares, as the release job logged for v0.4.0-alpha.4. The job checks the executable only on the runner’s macOS (macOS 26), not on macOS 12.

The Windows ARM64 executable is built and checked on the GitHub-hosted Windows 11 ARM64 runner, natively, not cross-built on x64. Releases before the first one that lists it have no win-arm64 files. The release workflow publishes the complete release-setup bundle for Windows x64 and Windows ARM64, with one SHA256SUMS over both.

Each archive holds the same executable under <rid>/, for example linux-x64/morphirup, with the repository’s LICENSE and NOTICE and the executable’s THIRD-PARTY-NOTICES.txt beside it. The release also carries that file as morphirup-v<version>-THIRD-PARTY-NOTICES.txt, for the bare executables: it lists the packages, the .NET runtime and the Unicode data that Native AOT compiled in, with their licenses and notices. The file morphirup-v<version>.sha256 lists the SHA-256 of every morphirup asset of the release, the notices file included, in the format that sha256sum -c reads. It shows that the file you have is the file the release published. It is not a signature: verifying signed release metadata is later work.

On Linux:

version=0.4.0-alpha.5   # the release you want
base=https://github.com/finos/morphir-dotnet/releases/download/v$version
curl -fLO "$base/morphirup-linux-x64"
curl -fLO "$base/morphirup-v$version.sha256"
sha256sum -c --ignore-missing "morphirup-v$version.sha256"
chmod +x morphirup-linux-x64
./morphirup-linux-x64 --version

On macOS, compare the digest with its line in the checksum file:

shasum -a 256 morphirup-osx-arm64
grep ' morphirup-osx-arm64$' "morphirup-v$version.sha256"

On Windows, in PowerShell:

(Get-FileHash .\morphirup-win-x64.exe -Algorithm SHA256).Hash.ToLowerInvariant()
Select-String -Path ".\morphirup-v$version.sha256" -Pattern ' morphirup-win-x64.exe$'

The release job checks each executable on a GitHub-hosted runner of its platform. It runs the published file from a folder whose path has a space, with PATH reduced to the system folders and no DOTNET_* variable. The file must print the release version and make a plan-only setup whose plan digest matches a recorded one.

A Native AOT executable needs the glibc of the machine that linked it, or newer. So the Linux executables are linked in an ubuntu:22.04 image pinned by digest, which has glibc 2.35, and not on the runner. The release job reads the glibc versions each Linux executable needs (readelf --version-info) and fails if one is above 2.35. It then runs the same commands in debian:12-slim (glibc 2.36) and in the ubuntu:22.04 build image (glibc 2.35), after checking that neither image has .NET, libicu, libssl or libcrypto. ubuntu:22.04 ships OpenSSL (libssl3), so the check removes that package from a copy of the image first. It also checks that each archive holds exactly the four files above and that the notices name every package the compiler linked. Nothing more is checked: no other distribution, glibc version or operating-system version is tested.

The executables are not code-signed, and the macOS executable is not notarized. On macOS, a file that a browser downloads carries the quarantine attribute, and Gatekeeper refuses to open it because Apple cannot check it for malicious software; a file that curl downloads has no quarantine attribute. After you have checked its digest, you can remove the attribute with xattr -d com.apple.quarantine morphirup-osx-arm64. On Windows, SmartScreen can warn about an unsigned file that a browser downloads. Neither behavior has been checked for these files on a Mac or a Windows desktop.

The Windows and macOS ARM64 executables also set up and run a toolchain, as above. No release verifies signed metadata yet.

Implementation, deployment and toolset

A request names three things, and the manager never infers one from another:

  • the implementation is which Morphir host, such as dotnet;
  • the deployment is how that host is delivered: native-aot, self-contained, framework-dependent or dotnet-tool. Each is a separate offering with its own artifacts, so native-aot is never replaced by self-contained;
  • the toolset is the preset of language operations, such as fsharp, which needs F# compilation and F# generation. Each operation gets one provider: the host’s built-in, or an independently released process, wasm or dotnet-tool provider.

The project request is .morphir/toolchain.toml:

schemaVersion = "0.1.0-draft.1"
[host]
implementation = "dotnet"
deployment = "native-aot"
channel = "stable"
[toolset]
preset = "fsharp"

It is a separate file from morphir.toml. The [toolchain.<name>] task adapters in morphir.toml keep their meaning, and morphirup never reads or writes them.

Check in both .morphir/toolchain.toml and .morphir/toolchain.lock.json. The lock records the exact host and providers for each platform, with no local paths. Its requestSha256 is the SHA-256 of the request file’s exact bytes, so reformatting the request, even adding a blank line, needs a new lock. A colleague on another platform will extend the lock with an explicit setup for that platform; this build does not implement that yet, and morphirup setup --plan --platform <rid> only shows the plan. Frozen restore never resolves a channel again.

Commands

setup --plan (or setup --dry-run) and doctor write one manager result document (0.1.0-draft.1, see the contract, a draft in the unmerged finos/morphir#1147, at the commit this build is pinned to) to standard output and their progress to standard error. --version, --help and a command line that cannot be parsed write no result document. JSON is the only output format of this build; --json is accepted.

Exit codeMeaning
0The result is planned or succeeded
1The result is failed: a document was refused or a check failed
2morphirup did not run the command; see Exit code 2. Nothing is written to standard output; the reason is on standard error
3Provisional. Internal error. Nothing is written to standard output. The result contract defines exit codes 0, 1 and 2 only

Exit code 2

Exit code 2 means that morphirup did not run the command. That happens when:

  • no command is given, or the command is not setup or doctor;
  • an option is unknown, given twice, or has no value. A value never starts with --, so --project --plan is a missing value, not a folder named --plan;
  • setup is run without --plan or --dry-run, or with --yes;
  • --platform is not one of the eight platform ids, or this machine is not one of them and --platform is not given;
  • the value of --repository-id, --expect-plan, --implementation, --deployment, --preset, --channel or --version is not valid;
  • setup has no --repository-id or no --recipe, or doctor has --recipe without --repository-id;
  • the request is given two ways: --request with --project or with the selector options, or --project with the selector options;
  • the selector options are incomplete: --implementation, --deployment or --preset is missing, or not exactly one of --channel and --version is given;
  • the --artifact-sizes file cannot be read, is not a JSON object of objects, or has a size that is not a non-negative integer;
  • a downloaded artifact has no size in --artifact-sizes. setup finds this only after it has read the request and recipe and resolved the plan.

A request, lock or recipe that is absent or cannot be read is not exit code 2: the command runs and fails with document_unavailable (unsupported-contract, naming the document, at the empty path), exit 1.

Digests, repository ids and bounds

A plan carries the lock that setup would write, and the lock’s requestSha256 is the SHA-256 of the request file’s exact bytes. So a checkout that converts .morphir/toolchain.toml to CRLF line endings, or adds a byte order mark, gives another planSha256, and --expect-plan then fails with plan_changed. plan_changed names no cause. morphirup doctor does: its request_changed diagnostic says when line endings or a byte order mark are the likely cause.

In this build, --repository-id is copied as given into the plan’s source.repositoryId values and the lock’s recipe.repositoryId, and nothing checks it. The plan’s repository-target and recipe-digest sources label where each artifact would come from. They are not verified provenance: this build downloads and authenticates nothing.

A request, lock or recipe nested deeper than 64 levels is refused as invalid_document, whether the nesting is in JSON or comes from TOML arrays, inline tables or dotted keys.

A version, in a document, a comparator or --version, is at most 256 characters, and each numeric identifier (major, minor, patch, or an all-digit prerelease identifier) has at most 15 digits, as the contract states. A longer one is refused as invalid_value at its member before it is parsed. That holds for a MEP version (protocolVersion, protocolVersions) too: any text that is not a version in canonical SemVer spelling, such as 0.1, v0.2.0 or a patch of 16 digits, is invalid_value, and only a SemVer version off the supported 0.2 line, such as 0.1.0 or 0.3.0, is unsupported_case.

Files

morphirup reads a request, lock, recipe or sizes file only when it is a regular file. A directory, device, pipe or socket is refused, and so is a symbolic link to one, including a link into /dev, /proc or /sys. A file has five seconds to open, and then thirty seconds for each read: a file that keeps sending data is read in full, and one that sends no data for thirty seconds is refused as “is not a regular file: it sent no data for thirty seconds”. A refused request, lock or recipe is document_unavailable; an unreadable sizes file, which is this build’s own input, is exit 2.

A read error is reported in morphirup’s own words, never as a system message: “does not exist”, “cannot be read: it is in use by another process”, “cannot be read: access is denied”, “cannot be read: its path is too long for this system”, or “cannot be read: input or output failed” with the error code.

The size bounds are provisional, until the contracts state their own:

FileLargest size read
Request (.morphir/toolchain.toml or --request)1 MiB
Lock (.morphir/toolchain.lock.json or --lock)1 MiB
--artifact-sizes1 MiB
--recipe16 MiB

A larger file is refused before it is parsed.

Finding the project

Without --request or the selector options, morphirup finds the project request by discovery: the nearest .morphir/toolchain.toml in the start directory or one of its parents, up to and including the discovery boundary. Nothing above the boundary is read, and requests are never merged, so a workspace member with its own request uses it and a member without one uses the workspace root’s. The start is --project <dir>, or the current directory; the boundary is --boundary <dir>. A start outside the boundary has no project request. --project and --boundary are provisional.

Discovery stops at the first level where .morphir/toolchain.toml exists in any form: a file, a directory, a link (even one that points nowhere) or an entry that cannot be listed. Anything there that is not a readable regular file is refused as document_unavailable, and the request of an ancestor is not read instead.

Without --boundary, discovery searches up to the root of the file system: / on Linux and macOS, the drive’s root on Windows. So when no nearer request exists, a .morphir/toolchain.toml in any ancestor of the start directory is used, for example in your home directory, in /tmp or at the root of a shared drive. Give --boundary <dir> to stop at your workspace. Give --boundary in the same letter case as the path you start from: on Windows the comparison ignores case; elsewhere it is exact, even on a case-insensitive volume.

doctor reads the lock beside the request: the one it found, or the file given with --request, unless --lock names another.

Dry runs

Every morphirup command that creates, updates, repairs or removes persistent state has a --dry-run option. A dry run takes the same planning and validation path as the command itself. It refuses whatever the command would refuse, with the same codes, paths and exit codes, and it makes no persistent change: it writes no request, lock, receipt, journal, cache, home or activation file, downloads nothing and runs no readiness probe. Its result reports only what is true: an action it would take is planned, never succeeded, and an action it cannot complete is reported with the diagnostic that blocks it.

In this build, only setup could change state, and it only plans, so setup --dry-run is setup --plan under another name (see below). doctor reads and never changes anything, so it has no --dry-run. A future doctor --fix, and every later command that installs, activates, updates, rolls back, repairs or removes, will come with --dry-run from the start; none of them is implemented yet.

morphirup --version

Prints the version and nothing else.

morphirup setup --plan and --dry-run

morphirup setup (--plan | --dry-run) --recipe <file> --repository-id <sha256> --artifact-sizes <file>
                [--project <dir> [--boundary <dir>] | --request <file> | <selector options>]
                [--platform <rid>] [--expect-plan <sha256>] [--recipe-target <path>]

Reads the request, resolves it against the recipe for every platform the lock would record (the request’s platforms plus the platform being set up), and prints a planned result with the plan and its planSha256. The plan names the host artifact, each provider, where each comes from, its sizes, its update owner, the prerequisites that apply on its platform and its probe. It also carries the complete lock setup would write, with every platform’s closure, so planSha256 pins all of it. The host and every process or dotnet-tool provider are process-with-user-rights: setup’s readiness checks would run that code with your rights. A built-in or WASM provider is in-host. Planning runs nothing.

Plan-only setup is the only setup in this build: without --plan or --dry-run, or with --yes, it exits with code 2 and changes nothing. Installing is not implemented yet.

--dry-run is the same request as --plan: the same plan and planSha256, the same result bytes, the same refusals and the same exit codes. Both may be given together and mean the same; each may be given once. Neither permits --yes, so a dry run can never install or activate anything. A dry run reads only the request, recipe and sizes files, and creates, changes or removes nothing in the project or the Morphir home.

OptionMeaning
--project <dir>Provisional. Where discovery starts. The default is the current directory
--boundary <dir>Provisional. The discovery boundary. The default is the root of the start path
--implementation, --deployment, --preset, --channel or --versionThe request as options, as in the approved design. The approved example given this way has the approved example file’s identity
--platform <rid>Plan for this platform instead of the current one
--expect-plan <sha256>Refuse with a failed result (exit 1) when the plan’s digest differs: plan_changed at planSha256, naming both digests. Nothing is changed
--recipe <file>Provisional. The recipe document. Later builds take it from authenticated TUF metadata; this build trusts the file you give it
--repository-id <sha256>Provisional. The repository that publishes the recipe and the host artifacts. A recipe provider bound to another repository is repository_mismatch, a trust-failure diagnostic whose document and path name that provider’s repositoryId
--recipe-target <path>Provisional. The recipe’s target path, by default toolchains/recipes/<preset>/<version>.json. A target that is not portable, such as one with a con segment, is non_portable_path before the recipe is read
--artifact-sizes <file>Provisional. A JSON object from target path to {"downloadBytes": n, "installedBytes": n}. A plan needs both sizes for every downloaded artifact. Later builds take downloadBytes from the authenticated target length; the contract has not settled where installedBytes comes from
--request <file>Provisional. Read this request file

A request the recipe cannot satisfy gives a failed result with an unsupported-artifact diagnostic, whose document (request) and path name the request member it concerns, such as toolset.providers[1]. The manager never substitutes another implementation, deployment, release, platform or WASM ABI. A missing platform lists the recipe’s alternatives; a WASM provider whose ABI the host lacks is abi_unavailable unless the request chooses another ABI explicitly.

morphirup setup --plan --recipe fsharp-1.0.0.json --repository-id <sha256> --artifact-sizes sizes.json
morphirup setup --dry-run --recipe fsharp-1.0.0.json --repository-id <sha256> --artifact-sizes sizes.json
morphirup setup --plan --implementation dotnet --deployment native-aot --preset fsharp --channel stable `
    --recipe fsharp-1.0.0.json --repository-id <sha256> --artifact-sizes sizes.json --expect-plan <sha256>

morphirup doctor

morphirup doctor [--project <dir>] [--boundary <dir>] [--request <file>] [--lock <file>] [--platform <rid>]
                 [--recipe <file> --repository-id <sha256>]

Checks a project’s request and lock without changing anything:

  • both documents can be read (document_unavailable otherwise) and read as documents;
  • the lock binds the exact request bytes, hashed as read (request_changed otherwise). When the bytes differ only by CRLF line endings, a UTF-8 byte order mark or both, the diagnostic’s cause says so and its message names the .gitattributes entry, .morphir/toolchain.toml text eol=lf, that prevents it. A converted file is never accepted;
  • the lock records the current platform (platform_not_locked otherwise). An explicit setup for that platform will extend the lock; this build does not implement it yet. The diagnostic says so and names only morphirup setup --plan --platform <rid>, which shows the plan;
  • with --recipe and --repository-id, the lock names that repository (repository_mismatch otherwise, with document lock and the path of the member that names another one) and every closure is one the recipe allows (lock_recipe_mismatch). --recipe needs --repository-id, because the lock-against-recipe check takes the repository as an input.

A recipe that is refused is reported even when the lock is refused too. The result is succeeded with no diagnostics, or failed with every diagnostic found. Installed generations are not checked yet. --project, --boundary, --request, --lock, --recipe and --repository-id are provisional.

Recipe binding checks

Morphir.Toolchain can check a recipe’s bindings against the repository that authenticated it: every host and provider row of the toolset recipe page’s authenticated recipe targets. It reads three documents for this, each at the one version the toolchain contract lists, and no earlier one:

  • the tool release metadata v1 release descriptor of a tool-release host offering (schemaVersion 1);
  • the extension release descriptor a provider binds (schemaVersion 2.0.0-draft.2);
  • the capability claim set of each of its artifacts (claimsVersion 0.1.0-draft.2).

It also selects a tool release by the tool release metadata v1 rules for channels, CLI compatibility and platforms.

No morphirup command uses any of this yet. Nothing downloads anything, and nothing here verifies repository metadata: the checks take the authenticated targets and their bytes as inputs. This code has not had a security review. It must not be used to authenticate a production release until it has.

A provider’s claims say what its publisher claims the artifact does. Checking that they name the provider’s capability, MEP version, language or target and model tuple is not running the artifact. A recipe digest authenticates the bytes; the descriptor’s claimCheck says whether anyone ran them to read the claims. The two are kept apart, and a recipe never changes claimCheck.

Platforms and prerequisites

morphirup uses the .NET runtime identifiers win-x64, win-arm64, osx-x64, osx-arm64, linux-x64, linux-arm64, linux-musl-x64 and linux-musl-arm64. A musl machine never gets a glibc closure. These are the platforms it can plan for; releases publish the executable for five of them, listed under Download and verify.

The executable needs no .NET runtime, no ICU and no OpenSSL: it is built in invariant globalization mode, it normalizes strings for the plan digest to NFC with its own Unicode 17.0.0 tables, and it computes SHA-256 with BouncyCastle’s managed implementation. So it runs on a Linux machine without the libicu or libssl packages, and a plan digest is the same whatever Unicode version the machine has.

Common errors

For problems with installing, setup, show and run, see Troubleshooting. The errors below come from recipe planning (setup --plan) and doctor.

  • this build plans only: pass --plan or --dry-run, without --yes. Installing is not implemented yet.
  • document_unavailable: the named request, lock or recipe does not exist, is over the bound in Files, or is not a regular file. Without --request, check where discovery looked.
  • request_changed with cause crlf-line-endings: Git converted the request’s line endings. Add .morphir/toolchain.toml text eol=lf and .morphir/toolchain.lock.json text eol=lf to .gitattributes and check the files out again.
  • the size of <target> is unknown: add the target to the --artifact-sizes file.
  • unsupported_contract_version at schemaVersion: this build reads 0.1.0-draft.1 documents only, never another draft or the released 0.1.0.
  • credential_in_document: no toolchain document may hold a member such as token, password or apiKey, at any depth and in any letter case. The result names the member, never its value.

Repository metadata verification

Morphir.Toolchain has the verification part of a TUF client for the tool release metadata v1 profile: it reads root, timestamp, snapshot and targets metadata, checks Ed25519 signature thresholds, follows root rotation, and checks rollback, links and expiry. No morphirup command uses it yet, and nothing in this release downloads or authenticates repository metadata. This code has not had a security review. It must not be used to authenticate a production release until it has.

A trust anchor has a purpose, TestOnly or Production, and every root the client later accepts by rotation must meet the same rules as the anchor. A production anchor must be the repository whose identity the caller names: no root is production on the strength of the checks below alone, and no production identity ships with this build. The production checks then refuse:

  • a root threshold below two, at the anchor and at every rotation;
  • any of the five public keys of this repository’s TEST-ONLY signed fixture, wherever a root lists them.

That deny list stops those five keys and nothing else. It does not stop any other published test seed, of which this repository has several, and it says nothing about which repository is meant. That is why production also needs the repository identity.

12 - morphir extension worker (experimental)

Experimental and internal: serve one WASM extension module over stdio for a conformance runner

morphir extension worker

Experimental and internal. The worker exists so that a conformance runner can qualify this host’s WASM runtime with the crafted cases of the WASM execution profile contract (OpenSpec change add-wasm-extension-host, task 4.1). Product commands do not use it, and it is not in the help of morphir extension. Its arguments and events follow the pinned contract, which is still a draft, so they can change in any release. Contributors: see WASM host qualification.

Synopsis

morphir extension worker --abi <profile> --module <path> --build-identity <hex>
    [--request-bytes <n>] [--response-bytes <n>] [--call-ms <n>] [--instructions <n>]
    [--memory-pages <n>] [--json]
morphir extension worker --help [--json]

Both executables have it: morphir and dotnet-morphir, and so does the managed morphir built from src/Morphir.Managed, which is not published yet. Each starts the worker before it builds the rest of the command line, so the worker starts fast and nothing but MEP frames reaches stdout.

Description

The worker loads one module and relays MEP messages to it. Each Content-Length framed message on stdin is one handle call of the module. The reply to a request is written as one frame on stdout. A notification gets no frame, and reaches the guest only if the guest asked for notifications in its morphir.initialize result. The worker does not negotiate the protocol, and does not check the JSON-RPC version or ids: the runner at the other end is the MEP host.

The worker keeps reading until the module’s instance ends, at morphir.exit or at a fault, and then exits by itself. A runner keeps stdin open until then.

Before it serves, the worker checks the build identity (before the module file is read), then loads the module: it reads the file, computes its SHA-256, validates and compiles it, classifies it against the pinned contract and admits it with the declared ABI. Then it opens one instance under the bounds and writes started.

Options

OptionDefaultMeaning
--abi <profile>requiredThe declared profile, extism-kernel-v1 or morphir-bytes-v1. This host runs extism-kernel-v1 only and refuses a morphir-bytes-v1 module. A declaration only confirms the profile the imports give.
--module <path>requiredThe module file. A relative path is resolved against the current directory.
--build-identity <hex>requiredThis executable’s build identity, 64 hexadecimal digits, as --help --json reports it. Without it the worker serves nothing and exits with 2. Another identity is refused with extension.worker_runtime_mismatch.
--request-bytes <n>6291456 (6 MiB)The largest request frame, its Content-Length header included. A larger frame is fault failure, and the guest is not called.
--response-bytes <n>16777216 (16 MiB)The largest response frame, header included. A larger response is fault failure, and nothing of it reaches stdout.
--call-ms <n>30000 (10000 for morphir-bytes-v1)The deadline of each handle call, in milliseconds.
--instructions <n>5000000000The instruction budget of each call, in Wasmtime fuel units, from 1 to 9007199254740991, the range the contract states.
--memory-pages <n>4096 (256 MiB)The ceiling of the guest’s memory, in 64 KiB pages, from 1 to 65536. This host has one limit for every memory of a session, so it applies to the Extism kernel’s memory too, as the contract allows. Below the default the two hosts differ: the MoonBit host leaves the kernel at 256 MiB. A value outside the range is a usage error, exit 2, before the module is read.
--jsonoffWrite the report as morphir/command-event lines.
--help, -hWrite the help on stdout and serve nothing. With --json, write one morphir/command-result whose result.buildIdentity is the identity to pass.

The defaults and the range of --instructions are the pinned contract’s (tests/Fixtures/MepWasm/wasm-execution-profiles.json, worker.arguments). The contract states no other range; the others are this host’s own.

The build identity

The identity is the SHA-256 of a short text that names the pinned profile contract (id, version and SHA-256), the host (morphir or dotnet-morphir), the release version with its build metadata (the git commit of a CI or release build), the embedded Extism kernel (moonrockz/extism version and SHA-256) and the engine (wasmtime 50.0.1). Two builds of one version and commit share an identity, also when one of them has uncommitted changes.

$ morphir extension worker --help --json
{"profile":"morphir/command-result","contractVersion":"0.1.0","command":"extension worker","state":"succeeded","exitCode":0,"result":{"help":"...","buildIdentity":"e4d9...531d"},"diagnostics":[]}

Output

Stdout carries only Content-Length framed replies, in every case.

Stderr carries the report. With --json each event is one line:

{"profile":"morphir/command-event","contractVersion":"0.1.0","command":"extension worker","sequence":2,"event":"fault","fault":{"kind":"trap","message":"extension.guest_trap: the guest trapped: ..."}}
EventMembers after the envelopeWhen
startedabi, moduleSha256, buildIdentity, boundsThe module has loaded. bounds has exactly memoryCeiling, instructionBudget and callDeadline, each {"enforced": true}: whether the bound is enforced, not its value.
outputoutput; or output and droppedBytesGuest output of the call that just ended: WASI stdout and stderr, Extism and Morphir log messages, refused HTTP requests. The newest 64 KiB of a call are reported, each message cut to 4 KiB, then one event with droppedBytes for the rest.
faultfault.kind, fault.messageThe session failed. The kind is trap, memory-limit, budget-exhausted, call-deadline or failure. The message states the guest’s memory size, the kernel’s memory size and the ceiling. A fault has no code.
refusederror.code, error.messageA refusal before any request: extension.worker_runtime_mismatch, extension.runtime_unavailable, extension.abi_unsupported, extension.abi_mismatch or extension.wasm_feature_unsupported. The message of a module refusal names the file and its SHA-256.
exitedexitCodeLast, in every run that got past the arguments.

The envelope is profile, contractVersion, command and sequence (from 1), then event. A message in an event is cut to 4 KiB of JSON text and then ends with ….

No event carries the engine identity, with or without --json. The engine is part of the build identity, and a runner is given it as an input; session diagnostics and the runtime report state it. The contract fixes the event names and their members. The rest of the morphir/command-event and morphir/command-result documents, the help text and the usage-failure document follow the MoonBit CLI until a shared contract states them.

Without --json each event is one text line that starts with morphir extension worker: , for example morphir extension worker: serving extism-kernel-v1 module 013d...e11e.

A usage error serves nothing. With --json it is one morphir/command-result failure on stderr (error.code usage); without it, one text line on stderr.

A host without its WASM runtime

When this executable cannot run WASM modules, the worker checks that after the arguments and the build identity and before it reads the module. It writes one refused event with extension.runtime_unavailable, whose message names the host, the platform and the reason, writes nothing on stdout and exits with 3. --help and the identity check never load the engine. The reasons are those of morphir extension runtimes. The pinned profile contract lists the code among the worker’s refusals, and its worker rule start-order fixes this order: the arguments, the build identity, a usable engine, then the module.

Faults before started

A module file that cannot be read, or that is not a well-formed WASM module, is fault failure before started. So is a module whose initial memory is above the ceiling, with kind memory-limit. Each exits with 1. None of them is a refusal.

Message checks

The worker checks that each request and each response is UTF-8 text holding one JSON object. Anything else is fault failure; a request that fails the check never reaches the guest, and a response that fails it never reaches stdout.

Interruption

Ctrl+C (SIGINT) stops the guest within one second, even inside a call that spins. The worker writes no fault event, because the guest did not fail, and exits with 130. On Windows Ctrl+Break does the same, because a parent can send only Ctrl+Break to a child that runs in its own process group.

Private workspace

A guest that calls morphir_get_workspace_info gets an empty private directory. The worker creates its own directory in the system’s temporary directory (morphir-extension-worker-<hex>), the session creates the guest’s directory below it, and the worker removes both when it exits, also after a fault or an interruption.

Exit status

StatusMeaning
0The guest served until its instance ended.
1A fault, an invalid frame, or stdin ended first.
2Invalid arguments.
3A refusal before any request.
130Interrupted.

Example

$ id=$(morphir extension worker --help --json | jq -r .result.buildIdentity)
$ morphir extension worker --abi extism-kernel-v1 --module ./extism-echo.wasm --build-identity "$id" --json

13 - Troubleshooting

Solutions to common issues with Morphir .NET CLI

Troubleshooting Guide

C# generation

Use exact target csharp, numeric language-version=14 or 15, and an optional literal C# namespace. These invocations fail with exit 2 before publication:

morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target csharp --output artifacts/generated-csharp --option language-version=preview --json
morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target csharp --output artifacts/generated-csharp --option namespace=true --json

The first requires a numeric profile. The second supplies a JSON Boolean where a namespace string is required. Use --option language-version=14 and --option namespace=Pricing.Generated. Source syntax, empty segments and unescaped reserved words are refused with a located namespace diagnostic. Use @ for reserved words, for example namespace=@class.Models.

In JSON mode, parse the single stdout report and inspect failureKind and diagnostics. Structured diagnostics are report data; JSON failures need not duplicate their text on stderr. Without --json, human diagnostics use stderr. Exit 1 identifies unsupported or invalid model semantics; exit 2 identifies invocation or operational failures; cancellation before publication returns 130. No successful partial sources are published for these failures.

An unsupported expression, SDK operation, distribution or required semantic has a stable rule and available model location. Read the C# API/diagnostic guide, then correct the input or restrict it to the admitted model contract. Do not edit emitted sources to hide a refusal. For a compiler error, verify the separately selected compiler’s numeric language version and the Morphir.SDK.CSharp package reference using the fresh-consumer walkthrough.

Keep input and output separate and retain .morphir-generated.json. A failed generation or rejected overlap preserves earlier owned sources and unowned files. Shared publication does not promise rollback after a filesystem failure during final commit; inspect the report and rerun after fixing that failure.

This guide covers common issues you may encounter when using the Morphir .NET CLI and how to resolve them.

Installation Issues

Tool Not Found After Installation

Problem: After running dotnet tool install, the morphir command is not found.

Solution:

  1. Check if tools are in PATH:

    echo $PATH | grep .dotnet/tools
    
  2. Add to PATH if missing (Linux/macOS):

    export PATH="$PATH:$HOME/.dotnet/tools"
    
  3. Add to PATH if missing (Windows):

    $env:PATH += ";$env:USERPROFILE\.dotnet\tools"
    
  4. Restart your terminal after modifying PATH

Installation Fails with “Unable to find package”

Problem: dotnet tool install Morphir.CLI fails to find the package.

Solution:

  1. Check NuGet sources:

    dotnet nuget list source
    
  2. Add nuget.org if missing:

    dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
    
  3. Verify package name - ensure you’re using the correct package name

Validation Issues

“File does not exist” Error

Problem:

Error: File does not exist: morphir-ir.json

Solutions:

  1. Check file path:

    ls -l morphir-ir.json
    
  2. Use absolute path:

    morphir ir verify /full/path/to/morphir-ir.json
    
  3. Check current directory:

    pwd
    cd /path/to/ir/files
    morphir ir verify morphir-ir.json
    

Malformed JSON Errors

Problem:

Message: Malformed JSON: 'i' is an invalid start of a value. LineNumber: 6

Solutions:

  1. Validate JSON syntax with a JSON validator:

    cat morphir-ir.json | jq .
    
  2. Check for common issues:

    • Missing commas between array/object elements
    • Trailing commas (not allowed in JSON)
    • Unquoted keys or values
    • Invalid escape sequences
  3. Use a JSON formatter:

    cat morphir-ir.json | jq . > morphir-ir-formatted.json
    

Schema Version Detection Issues

Problem: Auto-detection selects the wrong schema version.

Solutions:

  1. Explicitly specify version:

    morphir ir verify --schema-version 3 morphir-ir.json
    
  2. Check formatVersion field (for v2/v3):

    cat morphir-ir.json | jq '.formatVersion'
    
  3. Verify IR structure:

    • v1: No formatVersion field
    • v2: "formatVersion": 2
    • v3: "formatVersion": 3

Validation Fails but IR Appears Correct

Problem: Validation reports errors, but you believe the IR is valid.

Investigation Steps:

  1. Review error messages carefully - they include Expected vs. Found values

  2. Check schema documentation - Schema Specifications

  3. Validate against different versions:

    morphir ir verify --schema-version 1 morphir-ir.json
    morphir ir verify --schema-version 2 morphir-ir.json
    morphir ir verify --schema-version 3 morphir-ir.json
    
  4. Compare with known-good IR:

    # Validate a reference file
    morphir ir verify reference-morphir-ir.json
    
  5. Check for common issues:

    • Incorrect tag capitalization (e.g., "Public" vs "public")
    • Missing required fields
    • Incorrect array structure
    • Type mismatches

Configuration

The shared libraries and morphir config get/list/show/path/explain provide multi-format resolution, bounded discovery and inspection. Configuration editing, classic project migration and bounded workspace migration use shared plan/apply services. Existing artifact/cache tooling retains TOML cache settings and Git-first discovery. See the guide for the library APIs and the command reference for read/write options and recovery semantics.

Wrong workspace or missing project

For a library host, inspect ProjectDiscoveryResult.Discovery, Selection, source reports, and diagnostics. Check the discovery boundary, the nearest declared workspace, member globs/exclusions, and the selected project. Use morphir config path --json to inspect this evidence. A project file does not hide a parent workspace. A Git root or an empty .morphir directory does not declare a workspace through the new API. If artifact-management commands use a different root, check their legacy Git-first policy before changing the declaration.

Ambiguous configuration files

Read every path named in the diagnostic. Keep at most one ordinary primary per format group across the root, hidden, and dot-config layouts. For example, morphir.yaml and morphir.yml conflict, as do morphir.toml and .morphir/morphir.toml. JSON, YAML, and TOML may coexist across groups. Supplemental morphir-workspace.json may coexist with the ordinary JSON primary.

A member is missing or cannot be selected

Inspect workspace.root, members, and exclude in the declared workspace. A missing literal member warns; an existing directory without configuration and an invalid member document remain identifiable as errors. Duplicate display names do not merge projects. Select the canonical member path to disambiguate. An explicit unknown name/path or an invalid default_member is an error, rather than a request to choose an arbitrary project.

A candidate was not read

A report of Missing means the optional path was absent. Failed retains a parse/access diagnostic and must not be treated as a miss. Discovery never follows discovered links, including a linked configuration directory. Check the host boundary and mount permissions. Inactive profiles, personal files under isolation, and machine sources under isolation are intentionally not read.

A machine file is ambiguous

Global candidates include the platform configuration directory and the alternate Morphir home. Two files in the same format group conflict even when they are in different directories. MORPHIR_HOME replaces only the home alternative. A relative XDG_CONFIG_HOME is ignored. Requesting non-isolated machine loading without MachineTree produces discovery.machine_host_required; pass a separately authorized tree together with the explicit machine locations. Neither API implies ambient machine-file access. Remove the unintended duplicate or use different format groups when deliberate layering is required.

A member output setting has no effect

workspace.out_dir belongs to the owning workspace. Member output overrides produce diagnostics and cannot relocate it. Read the separate owning workspace context rather than expecting the root’s workspace subtree to be inherited into the member document.

A setting still has its old value after an edit

For current hosts, inspect the resolved origins and source reports. Use morphir config explain <key> to identify contributing layers and the winning source. A successful scoped write can remain overridden by a member, profile, personal file, environment variable, or explicit CLI value. Check the target reported by the edit and distinguish scoped inspection from effective inspection.

A value reappears after unset

unset removes the entry only from its target TOML file. A lower-format file, inherited scope, or default can supply it again. Use get <key> --show-origin to find the newly effective source. Removing an override does not delete the inherited setting.

A profile did not load

Check the supplied ProfileSelection, MORPHIR_PROFILES, and base profile controls. The CLI equivalents are --profiles and --no-profiles. An empty profile environment selector disables profiles, and inactive files are not read. Named profiles are not activated by CI detection alone. Profile stems must be adjacent to an eligible primary layout; a dot-config primary uses config.profile.<name>.<ext>. Isolation retains active profiles while omitting machine and personal sources. For the existing loader’s separate CI behavior, inspect CiProfileMode and .morphir/morphir.ci.toml.

A scoped key is missing or a value is redacted

A scope reads only that source group. A workspace-scoped miss does not inherit a global value. Compare get <key> --workspace with get <key> --show-origin. Empty strings and arrays are present and exit 0; an absent queried key exits 1. Quote a segment containing a literal dot, for example 'codegen."custom.generator".enabled'.

[REDACTED] is safe presentation for sensitive names and opaque secret references. Inspection never executes a secret command. Parser diagnostic messages are sanitized because they can contain values; use diagnostic codes, positions and source paths to locate the problem. --json returns structured diagnostics while stderr retains safe human diagnostic text.

Microsoft configuration projection is rejected

The optional adapter rejects case-distinct paths, literal : separators and empty segments that cannot retain their canonical identity in .NET’s hierarchical view. Use the typed canonical result to inspect these names; registration must not silently overwrite them. See the adapter reference for projection diagnostics, generated array binding and published host qualification. Pinned dependency linker warnings remain visible; successful smoke execution is not a warning-free claim.

A configuration write fails

Check the reported target, permissions, parse/validation diagnostics, and whether another process changed the file. Explicit YAML/JSON file targets are readable but not writable through these commands; select a scope with a TOML target. For a concurrent-change conflict, reload and review the new contents before retrying. Failed validation or a write conflict must leave the file unchanged.

--json changes output only; it does not infer input types. Use --type bool for ui.color, --type int for an integer, or complete JSON arrays/objects with their explicit type. Nulls, duplicate object keys and out-of-range integers cannot be written as these TOML edit values. A missing unset exits 1 even when another source supplies the effective value.

At a workspace root, an automatic edit targets the workspace primary. --user follows the selected project, including default_member. Use the reported target before applying a preview. A successful root write can remain overridden by a selected member, profile, personal file or environment source; read the nested effective explanation and winning origins.

A stale plan includes more than a changed destination. New alternate primaries, changed lower-format sources, active-profile inputs and relevant directory inventories also invalidate it. Reload and review before retrying. Identical writes do not touch the timestamp.

Native trees do not follow links. On macOS /etc is a conventional symlink, so a default system edit can fail before writing. An explicitly authorized physical TOML path avoids that alias; do not relax a discovery boundary or follow an unrelated link to hide the error.

rolled-back is a failure with the original target restored and owned temporary data removed. recovery-required identifies retained stage, backup, journal or directory artifacts with their transaction owner. Preserve them and inspect the target/journal before restoring a matching backup. A cleanup failure can retain artifacts after the data outcome is already known. Do not assume an I/O failure means unchanged bytes, and do not delete recoverable data or another transaction’s artifacts. Native coordination is machine-local; a multi-machine store requires a host-supplied coordination protocol. Per-file atomic replacement does not imply multi-file group atomicity.

A classic project migration is blocked

Use morphir project migrate morphir.json --dry-run to inspect mappings, target conflicts, and verification results. Resolve conflicting TOML values deliberately. Nonempty legacy dependency or decoration structures need supported mappings; migration must not discard them merely because compatibility loading ignores them. An equivalence failure can also indicate a missing legacy default or changed relative-path meaning.

Validation and stale-plan failures leave both files unchanged. Verification excludes the classic source and ambient machine, personal, environment, CLI, and activated-profile overlays, so success demonstrates that the new file stands on its own in the declared base workspace context. Keeping morphir.json afterward is intentional for older consumers; it is not a failed migration. See the project command reference.

Workspace migration has a blocked member or needs recovery

Use morphir workspace migrate <directory> --dry-run to inspect the bounded inventory and all member plans. Exclusions and nested workspaces constrain what can be adopted. A conflict in one selected project blocks the batch before destination replacement.

If a commit fails after some replacements, inspect the reported restored/unresolved paths and retained recovery journal. Overlapping writes stay blocked until recovery is resolved. Do not delete recovery data to force another run. Successful application and complete rollback remove temporary transaction artifacts. See the workspace reference.

Performance Issues

Validation Takes Too Long

Problem: Validation of large IR files (>1MB) takes several seconds.

Current Limitations:

  • This is expected behavior for large files in Phase 1
  • Schema validation is inherently slower for complex, deeply-nested JSON

Workarounds:

  1. Use --quiet mode to reduce output overhead:

    morphir ir verify --quiet morphir-ir.json
    
  2. Validate in parallel (for multiple files):

    find . -name "*.json" | xargs -P 4 -I {} morphir ir verify --quiet {}
    
  3. Phase 2 improvements (coming soon):

    • Performance optimizations for batch processing
    • Caching and incremental validation

High Memory Usage

Problem: Validation consumes excessive memory for very large IR files.

Solutions:

  1. Check file size:

    ls -lh morphir-ir.json
    
  2. For files >10MB:

    • Consider splitting into smaller modules
    • Report the issue with file size details
  3. Increase available memory (if using Docker):

    docker run --memory 2g morphir-cli ir verify morphir-ir.json
    

CI/CD Integration Issues

CI Pipeline Doesn’t Fail on Invalid IR

Problem: Pipeline continues even when validation fails.

Solution: Check exit codes explicitly:

# GitHub Actions
- name: Validate IR
  run: |
    morphir ir verify morphir-ir.json
    if [ $? -ne 0 ]; then
      echo "Validation failed"
      exit 1
    fi

Or use set -e in bash scripts:

#!/bin/bash
set -e  # Exit on any error

morphir ir verify morphir-ir.json
echo "Validation succeeded!"

JSON Output Not Parsed Correctly

Problem: CI tools can’t parse JSON output.

Solutions:

  1. Verify JSON format:

    morphir ir verify --json morphir-ir.json | jq .
    
  2. Save to file:

    morphir ir verify --json morphir-ir.json > validation-result.json
    
  3. Check for extra output:

    • Use --quiet with --json to suppress non-JSON output
    • Some logging may appear on stderr

Permission Denied in Docker

Problem:

Permission denied: /app/morphir-ir.json

Solution: Fix file permissions or use volume mounts:

# In Dockerfile
RUN chmod +r /app/*.json

# Or when running
docker run -v $(pwd):/app:ro morphir-cli ir verify /app/morphir-ir.json

Error Message Reference

Common Validation Errors

Missing Required Field

Path: $.distribution
Message: Required properties ["formatVersion"] are not present
Expected: required property
Found: undefined (missing)

Fix: Add the missing field to your JSON:

{
  "formatVersion": 3,
  "distribution": [ ... ]
}

Type Mismatch

Path: $.modules[0].name
Message: Value is "string" but should be "array"
Expected: array
Found: string

Fix: Change the field type:

// Incorrect
"name": "MyModule"

// Correct
"name": ["my", "module"]

Invalid Tag Value

Path: $.modules[0].accessControl
Message: Value must be one of: ["Public", "Private"]
Expected: "Public" or "Private"
Found: "public"

Fix: Use correct capitalization:

// Incorrect
"accessControl": "public"

// Correct
"accessControl": "Public"

Array Structure Error

Path: $.distribution[2]
Message: Expected array to have exactly 4 elements
Expected: array of length 4
Found: array of length 3

Fix: Ensure array has the correct number of elements per schema.

Reporting Issues

If you encounter an issue not covered here:

  1. Check existing issues: GitHub Issues

  2. Gather information:

    • Morphir .NET CLI version: morphir --version
    • .NET SDK version: dotnet --version
    • Operating system and version
    • Complete error message
    • Minimal reproduction steps
  3. Create a new issue with:

    • Clear title describing the problem
    • Steps to reproduce
    • Expected vs. actual behavior
    • Environment information
    • Sample IR file (if possible) or minimal example

Management Commands (dist/tool/extension)

No Active Version Set

Problem: Running morphir dist which or morphir tool which <name> shows “No active version set”.

Solution:

  1. List installed versions:

    morphir dist list
    morphir tool list
    
  2. Set an active version:

    # Global
    morphir dist use <version>
    morphir tool use <name> <version>
    
    # Local (project-specific)
    morphir dist use <version> --local
    morphir tool use <name> <version> --local
    

Platform Not Supported

Problem: Installation fails with “Platform not supported” or downloaded artifact doesn’t work.

Solution:

  1. Check current platform:

    dotnet --info | grep RID
    
  2. Manually specify platform:

    morphir dist install <url> <version> --platform linux-x64
    
  3. Verify available platforms: Check the distribution documentation for supported platforms.

Local vs Global Confusion

Problem: Setting a version locally but it’s not being used.

Solution:

  • Remember precedence: Local (.morphir/) overrides global (~/.config/morphir/)
  • Check both scopes:
    morphir dist which          # Shows resolved version (local > global)
    morphir dist list           # Shows global installations
    morphir dist list --local   # Shows local installations
    
  • Clear local selection: Remove the active selection file in .morphir/dist/active or .morphir/tools/active-<name>

Installation Directory Not Created

Problem: Installation fails because parent directories don’t exist.

Solution:

The CLI should create directories automatically. If this fails:

  1. Manually create directories:

    # Global
    mkdir -p ~/.config/morphir/{dist,tools,extensions}
    
    # Local
    mkdir -p .morphir/{dist,tools,extensions}
    
  2. Check permissions:

    ls -ld ~/.config/morphir
    

Downloaded Artifact Is Corrupted

Problem: Installation completes but artifacts don’t work or are corrupted.

Solution:

  1. Check manifest for hash:

    cat ~/.config/morphir/dist/<platform>/<version>/manifest.json
    
  2. Verify download integrity (if SHA256 is in manifest):

    sha256sum ~/.config/morphir/dist/<platform>/<version>/bin/artifact
    
  3. Re-download:

    morphir dist remove <version>
    morphir dist install <url> <version>
    

Compile failures

Use --json and read failureKind and diagnostics, or --json-lines and read the last completed record. Exit code 1 is a source or model error; exit code 2 is an invocation, configuration, selection or I/O failure. See compile.

CodeCauseFix
compile.frontend_unavailableNo registered frontend reads the language. The Native AOT morphir has no built-in F# frontend.Use dotnet-morphir, which has a built-in F# frontend, or configure the F# MEP guest with --extension morphir-fsharp-frontend or [frontend.fsharp].extension.
extension.unavailableThe frontend extension did not start, or its session did not initialize.Check the --extension or [frontend.<language>].extension value: a command on PATH, a path, or a .dll. Read the guest’s stderr.
extension.not_admittedThe agreed MEP session does not admit the request, for example its IR version or a file extension.Request a version or documents that the message lists for the extension.
extension.tuple_unavailableThe host and the extension agreed no compileModel tuple (profile morphir-ir, codec morphir-json).Use an extension that serves JSON compile tuples for IR 4.0.0 or 3.0.0.
morphir.unrepresentableThe requested classic IR version cannot hold a value of the model: an integer outside the 64-bit range, or a character outside the Basic Multilingual Plane. Exit code 1.Request IR version 4.
compile.unknown_exposed_moduleproject.exposed_modules names a module that the sources do not define.Fix the module name, or remove it from exposed_modules.
msbuild.deadline_exceeded, msbuild.output_limit_exceededProject evaluation did not end before the deadline, or wrote more output than the limit.Check the project file for imports that run long or write much output.
extension.protocol_errorThe extension broke the MEP protocol, for example it wrote a log line to stdout.Fix or update the extension. Logs go to stderr.
extension.timeoutThe extension did not answer before the deadline.Check the guest’s stderr, and run the compile again.
compile.unsupported_ir_versionNo writer declares the requested IR version, for example 4.1.0-draft.1.Use a declared version: 1, 2, 3, 4 or 4.0.0.
compile.ir_version_not_servedThe selected frontend does not produce the requested IR version.Request a version that the message lists for the frontend.
compile.language_requiredNo language is selected, and the sources do not select one. A [frontend.<language>].project_file is read only for the selected language.Use --language, or set frontend.language.
compile.package_requiredNo package name.Use --package-name, or set project.name.
compile.unsupported_settingAn option or setting that this build does not implement, for example --types-only or ir.layout = "document-tree".Remove it. The message names the option or the key and its file.
compile.invalid_arguments naming --target or --generate-output--target without --generate-output, --target more than one time, or --generate-output or --option without --target.Give one --target with --generate-output <directory>. -o installs the IR; it is not the generation output.
incompatible-model after a successful compile with --targetThe backend of the target does not declare the IR version. The IR stays published.Use an IR version that the message lists for the backend, or run generate later with another target.
compile.generation_unavailable--target in a build that registers no generation backends.Use dotnet-morphir or morphir, which both register the F# and C# backends, or run generate on the published IR.
sources.conflicting_source_options--project-file and --input together.Use one of them.
sources.project_file_as_inputAn --input file is a project file.Use --project-file.
sources.unknown_project_fileNo project-file provider recognizes the file.Name a supported project file. See project-file sources.
sources.no_documentsThe source directory has no file with an extension that the frontend claims.Set project.source_directory, or use --input.
sources.unsupported_entry, sources.invalid_utf8A source, or a directory above it, is a link or a special file, or the source is not valid UTF-8.Name the target file, or save the file as UTF-8.
install.foreign_content-o names a directory that has a file that an earlier install of this task did not write. The directory does not change.Remove or move the file, or use another -o directory.
compile.record_missinggenerate without --input found no compile record, or the most recent compile failed.Run morphir compile first, or give --input.

If F# reports that a module or value is not defined, check the source order. A directory gives files in lexical order, and F# compiles files in order. Use --input files in dependency order, or a project file.

Generation failures

morphir generate requires explicit target and output paths. With --input, it does not read morphir.toml or morphir.yml. Without --input, it reads the IR of the most recent morphir compile of the project. Use exact target fsharp and Boolean option --option optimize=false to disable folding. Unknown options and quoted strings where Booleans are required return exit 2.

Use JSON/Ion bare IR, or an Ion tree rooted at manifest.ion. YAML models, JSON/YAML trees and MEP envelopes have explicit unsupported-format/profile diagnostics. A codec error identifies the input or tree member and logical location. A semantic refusal returns exit 1 and preserves previous output.

Keep input and output paths separate and remove links from output paths. Linked ancestors are also refused. On macOS, system temporary paths under /var can pass through an OS-provided link. Use the physical path or, in CI, a runner-owned temporary directory for model fixtures and generated output. Publication owns only paths listed in .morphir-generated.json. Staging failures preserve previous output; commit-time failures can leave mixed files. Fix the reported filesystem problem and rerun generation. In --json mode, parse stdout and collect logs from stderr separately.

See morphir generate for the complete command contract.

Getting Help

See Also

Project migration blocks or cannot write

Run morphir project migrate --dry-run --json and inspect ledger, conflicts, verification and diagnostics. Unsupported nonempty classic shapes and values TOML cannot represent must be reconciled explicitly. A conflicting target is never overwritten, even when an ambient override makes normal loading look equal. Use a supported same-project primary layout and retain one TOML primary.

A stale plan requires a fresh preview. Source, target, alternate-primary and base workspace evidence must still match before installation. Exit 2 also covers write failures. A rolled-back outcome restored the original target; a recovery-required outcome names owned journal, stage and backup artifacts. Preserve them and later user edits until manual recovery is resolved. Migration retains the original classic source and never edits workspace membership.

Workspace migration blocks or needs recovery

Run morphir workspace migrate <directory> --dry-run --json and inspect inventory, projects, diagnostics, and verified. The selected directory is the boundary even if it has no workspace. A child invocation does not adopt its parent or siblings. Excluded candidates, default skipped directories, links, and nested workspace boundaries appear in the inventory. Include/exclude controls are root relative and repeatable; existing workspace exclusions stay effective.

One unsupported classic field or conflicting TOML value blocks the entire batch. Resolve the reported project blocker and create a fresh preview. Migration retains classic JSON bytes, uses the single-project loss ledger, and excludes ambient personal/machine/profile/environment settings from equivalence checks. An adoption effect for shared output roots does not permit source-directory or setting drift. Generated artifacts remain where they are.

A conflict means input, target, listing, or topology evidence changed after planning. Replan after reviewing that change. Do not weaken filesystem boundary or link guards. On macOS use physical /private/tmp rather than its linked aliases.

recovery-required is unfinished work. Preserve all listed journals, stages, backups, and user-modified targets. Overlapping configuration edits, project migration, and workspace migration remain blocked until explicit recovery resolves the journal. uncertainDirectories identifies directories whose creation receipt was interrupted. Do not delete them even if they are empty. Explicit recovery can preserve reviewed uncertain directories and reports them in preservedDirectories. Inspect the shared transaction recovery report before choosing a recovery action. It identifies restored and unresolved paths; unresolved user changes are preserved. See workspace API and recovery for the supported API. There is no claim of cross-file crash atomicity, and silently deleting artifacts is not recovery.

Every JSON result, including parse or host-initialization failure, follows the workspace result schema. Exit 0 means applicable preview, success, or valid-workspace no-op. Exit 2 means blocked, invalid, failed, or recovery required. Diagnostics go to stderr and stdout contains one JSON document when --json is supplied.

14 - Compile

Compile source code to Morphir IR

Compile

compile reads source documents, compiles them with a frontend, writes the IR in the requested format version, and publishes it under the out root. With --target, it then generates code from the IR that it wrote, as generate does. The dotnet tool dotnet-morphir has a built-in F# frontend. The Native AOT executable morphir has no built-in frontend. A managed morphir built from src/Morphir.Managed has the same in-process F# frontend as dotnet-morphir, but it is not published yet.

Both executables can also run a frontend as a MEP 0.2.0-draft.1 extension: a guest process that the command starts and that compiles the documents. See Frontend extensions.

Synopsis

dotnet-morphir compile [--language <id>] [--input <file>... | --input <directory> | --project-file <file>]
  [--package-name <name>] [--ir-version <version>] [--project <path-or-name>] [--config <file>]
  [--extension <id-or-path>] [-o <directory>] [--out-dir <directory>]
  [--target <id> --generate-output <directory> [--option <key>=<value>]...] [--json | --json-lines]

Options

OptionMeaning
--language, -lSource language, for example fsharp. Overrides frontend.language.
--input, -iSource files in compilation order, or exactly one directory.
--project-fileOne build-tool project file that names the ordered sources. See project-file sources.
--package-namePackage name, for example acme/car-rental. Overrides project.name.
--ir-versionIR format version: 1, 2, 3, 4, or a SemVer version such as 4.0.0. Overrides ir.format_version.
--projectA project directory, a workspace member path, or a project name.
--configAn explicit configuration file.
--extensionA frontend extension: a command name, a path, or a .dll. Overrides [frontend.<language>].extension.
--output, -oInstall a copy of the IR in this directory after a successful compile. See Output.
--out-dirThe out root. Overrides MORPHIR_OUT_DIR and workspace.out_dir.
--targetA generation target, for example fsharp or csharp. Generate code from the compiled IR. See Compile and generate.
--generate-outputThe owned output directory of the generation. Necessary with --target.
--optionA backend option KEY=VALUE for --target, as in generate. You can give it more than once.
--jsonWrite one JSON report to stdout.
--json-linesWrite the compile events to stdout as JSON Lines, and end with the report.
--types-onlyNot implemented. The command refuses it with exit code 2.

Do not use --json and --json-lines together. Do not use --project-file and --input together. Give --target one time only, and always with --generate-output. --generate-output and --option need --target. The command refuses these errors with exit code 2 before it compiles.

Settings

Each setting comes from the first source that supplies it:

  1. a command-line option;
  2. the selected project configuration: project.name, project.source_directory, project.exposed_modules, ir.format_version, frontend.language, and the [frontend.<language>] keys project_file and extension;
  3. a default: IR version 4, source directory src, all public modules exposed.

--project and --config select the project through the usual project discovery. Outside a project, only the command line and the defaults apply. The report gives the origin of each setting in origins.

[project]
name = "acme/car-rental"
exposed_modules = ["Pricing"]

[ir]
format_version = 4            # or "4.0.0"; a declared pre-release such as "4.1.0-draft.1" is also valid

[frontend]
language = "fsharp"

[frontend.fsharp]
project_file = "src/Acme.CarRental.fsproj"   # the same key in every [frontend.<language>] table

A language id can contain a dot; then the id is one quoted TOML key, [frontend."acme.lang"]. Without the quotes, [frontend.acme.lang] is a table lang inside a table acme, which no language reads.

The upstream default of frontend.language is not a selection. If you do not set the language, the extensions of the documents select it.

IR versions

The command keeps the requested version exactly. It never reduces a version to its major number. 4 and 4.0.0 select the same writer, and the report records the version as you wrote it. A pre-release, for example 4.1.0-draft.1, matches only a writer that declares that exact version. This build declares 1, 2, 3 and 4. A version that no writer declares fails before compilation with compile.unsupported_ir_version and exit code 2.

Sources

The command selects sources in this order:

  1. --project-file: one project file;
  2. --input: files in the given order, or one directory;
  3. [frontend.<language>].project_file of the selected language;
  4. the files of project.source_directory.

A directory gives its files in lexical order of their relative paths, with / separators. It gives only files whose extension a registered frontend claims: .fs and .fsi for the in-process F# frontend, and .fs only for the F# MEP guest. The command does not search for project files, and it does not apply rules that belong to one language. F# compiles files in order, so use a project file or --input files when a file uses a module of another file.

The command reads each document as strict UTF-8. It refuses links and special files, and it names each refused document.

Frontends

The command selects a frontend by what each registered frontend advertises: the language (or the extensions of every document), the file extension of each document, the output IR version, and support for several documents. If no frontend matches, or more than one matches, the command fails before compilation and names the frontend and the capability.

  • dotnet-morphir registers the in-process F# frontend morphir-dotnet.fsharp.
  • The Native AOT morphir has no F# frontend. A compile of F# fails with compile.frontend_unavailable.
  • The managed morphir built from src/Morphir.Managed registers the same in-process F# frontend as dotnet-morphir. It is not published yet.
  • A configured extension replaces the registered frontends for the run. See Frontend extensions.

Frontend extensions

A frontend extension is a guest process that speaks the Morphir Extension Protocol (MEP) 0.2.0-draft.1 over stdio and serves morphir.frontend.compileModel. The F# frontend is available as the guest morphir-fsharp-frontend. Install it as a dotnet tool. It needs the .NET runtime:

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

Select the extension with --extension, or with [frontend.<language>].extension in the project configuration. The option overrides the key.

morphir compile --language fsharp --input src/Rates.fs --package-name acme/demo --extension morphir-fsharp-frontend
[frontend.fsharp]
extension = "morphir-fsharp-frontend"

The command reads the value in this way:

  • A name without a directory is searched only in the absolute directories of PATH, in order, and the command starts that absolute path. It is never taken from the invocation directory or from the directory of morphir itself. Starting a bare name directly would look there first: CreateProcess searches the application’s directory and the current directory before PATH on Windows, and .NET does the same on Unix. On Windows the name matches a .exe or .com file, in PATHEXT order, and batch files are not started. On Unix the file must be executable. A name that is not on PATH fails with extension.unavailable; to run a file in the current directory, give a path such as ./guest.
  • A relative path is relative to the invocation directory.
  • A path that ends in .dll runs on the dotnet host: DOTNET_HOST_PATH when it is set, else dotnet searched on PATH in the same way.

What configuration can run. [frontend.<language>].extension names a program that morphir compile starts with your user’s rights. The project configuration is the morphir.toml found from the working directory upward, so running morphir compile inside a checkout that you do not trust can start a program that the checkout chooses: a file inside the checkout, or any command on PATH. MORPHIR_ISOLATED=1 leaves out only the machine and personal configuration files, not the project’s own file, and MORPHIR_DISCOVERY_BOUNDARY only limits how far up the search goes. Read the extension keys of a project before you run morphir compile in it, or choose the file with --config.

The command starts the guest with explicit arguments and no shell, and passes the guest’s stderr to its own stderr. It agrees the session with initialize, and it admits only the model tuples that both sides support. The F# guest claims the language fsharp with the file extension .fs, several documents in one request, and these tuples: profile morphir-ir, codec morphir-json, envelope 0.1.0-draft.1, IR 4.0.0 and 3.0.0. IR versions 1 and 2 have no tuple. For them, the command compiles at 3.0.0 and writes the classic model at the requested version. Before it sends a request, the command checks that the session admits the language, the file extension of each document, the number of documents and the IR version. The guest does not claim .fsi signature files.

The command checks the model envelope of the result (base64, length and SHA-256) before it reads the IR. When the run ends, it sends shutdown and then exit. A guest that does not answer shutdown, or does not take exit, within the deadline (for example a guest that is still busy or no longer reads its input) has its whole process tree stopped. So a request that times out ends within about two deadlines: one for the request and one for the end of the session. The report names the frontend with its transport, for example org.finos.morphir.fsharp-frontend (mep). For the same documents, the guest gives the same IR and the same diagnostics as the in-process frontend. A .fsi signature file is the exception: no frontend in the session claims it, so the command refuses the compile before it sends a request. Without --language, the refusal is compile.language_required.

An extension failure is exit code 2 with one of these codes:

CodeMeaning
extension.unavailableThe guest did not start, the session did not initialize, or the guest gave an id reserved for built-ins (every id that starts with morphir.).
extension.not_admittedThe agreed session does not admit the request, for example its IR version.
extension.tuple_unavailableThe host and the guest agreed no compileModel tuple, so no request can be sent.
extension.protocol_errorThe guest broke the protocol, for example it wrote a log line to stdout.
extension.timeoutThe guest did not answer before the deadline.

Upstream qualification: the MEP guest kit of finos/morphir still needs a legacy 0.1 stage, so the guest is not qualified with it yet. The guest is checked against the pinned 0.2.0-draft.1 contract and the conformance corpus. See the F# frontend.

Output

A compile writes its IR to <out root>/[<member path>/]compile.dest/morphir-ir.json and its task record to compile.json beside that directory. These files use the out-directory contract of the Morphir ecosystem. The out root is the first of:

  1. --out-dir;
  2. MORPHIR_OUT_DIR, relative to the invocation directory;
  3. the workspace workspace.out_dir, relative to the workspace root;
  4. .morphir/out under the workspace root.

Without a workspace, the project root is the workspace root, or the invocation directory outside a project. The member path is the directory of a workspace member relative to the workspace root, for example packages/orders.

The record has schema 1, task compile, the language, the project file in inputs (relative to the workspace root), value, the ir descriptor (path, layout, format and version, for example v4), the installed ledger and completedAt. morphir generate without --input reads the IR through this descriptor. See Generate.

When a compile starts, it clears compile.dest and replaces an earlier record with a tombstone. A successful compile then writes the full record. A failed compile publishes no IR and leaves the tombstone, which keeps the installed ledger. The compile holds the exclusive lock compile.lock until its record and its install are written. Another run that waits for the lock writes one line to stderr:

waiting for another Morphir run to finish this task (<out root>/compile.lock)

-o <directory> installs a copy of the IR in the directory after a successful compile. It replaces only the files that an earlier install of this task wrote there, and it keeps all other files. If a file that it must write is already there and an earlier install did not write it, the command fails with install.foreign_content and exit code 2, and the directory does not change. The compile output in compile.dest stays published.

Compile and generate

compile writes its task output and record, and can install IR or generated files. It currently has no --dry-run option; --option format=false controls formatting of generated F# only.

With --target <id> --generate-output <directory>, a successful compile continues into generation in the same run. The command gives the exact IR bytes that it wrote to the model loader of generate --input, and then runs the generation stages of generate: backend selection, --option validation, generation and owned publication. The result is the same as these two commands:

dotnet-morphir compile ...
dotnet-morphir generate --input <out root>/compile.dest/morphir-ir.json --target <id> --output <directory>

Both give the same files with the same bytes, and both reports name the same backend, as long as the project configuration selects no backend extension. compile --target always runs the built-in backend of the target and does not read [codegen.<target>].extension; when that key is set, the command prints a note on stderr, and only morphir generate runs the extension. Classic IR versions (1, 2 and 3) give a classic model, and the backend uses its classic path. As in generate, the generation member says whether the built-in backend formatted the files (formatting: applied, off or unavailable), and a host that cannot run the formatter (the Native AOT morphir) writes a warning on stderr, never on stdout or in the JSON output. Pass --option format=false for the unformatted files without the warning.

  • Generation does not run when the compile fails.
  • When generation fails, the compiled IR stays published. The report has success: false, the output of the IR, the modules, and a generation member that names the target and, after backend selection, the backend. A backend that does not accept the IR version fails before generation, names the backend and the versions that it declares, and does not change the generation output.
  • A generation failure has the exit code of generate: 1 for a model that the backend cannot use, 2 for an invocation, option or publication failure, 130 for cancellation.
  • -o installs the IR. It is not the generation output. A run with more than one target is not part of this command.
$ dotnet-morphir compile --language fsharp --input src/Rates.fs src/Totals.fs --package-name acme/demo --target fsharp --generate-output generated --json
{
  "success": true,
  "exitCode": 0,
  "language": "fsharp",
  "frontend": "morphir-dotnet.fsharp (in-process)",
  "irVersion": "4",
  "sources": {
    "projectFile": null,
    "root": "src",
    "documents": [
      "Rates.fs",
      "Totals.fs"
    ]
  },
  "modules": [
    "Rates",
    "Totals"
  ],
  "output": "/work/.morphir/out/compile.dest/morphir-ir.json",
  "installed": [],
  "diagnostics": [],
  "failureKind": null,
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "default"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  },
  "generation": {
    "target": "fsharp",
    "backend": "morphir.fsharp",
    "output": "/work/generated",
    "sourceVersion": "4",
    "family": "V4",
    "distributionKind": "Library",
    "representation": "Json",
    "artifacts": [
      "Rates.fs",
      "Totals.fs"
    ],
    "written": [
      "Rates.fs",
      "Totals.fs"
    ],
    "unchanged": [],
    "removed": [],
    "formatting": "applied"
  }
}

In human mode, the summary on stderr also names the backend, the number of generated artifacts and the generation output. Stdout still holds only the path of the published IR.

Exit codes

CodeMeaning
0Success. Warnings do not change the exit code.
1The frontend, an IR builder or the writer reported a source or model error.
2An invocation, configuration, project-file, selection, extension or I/O failure.
130The command was cancelled before publication.

Human output

Without --json or --json-lines, the command writes each stage’s diagnostics to stderr when that stage completes, before later stages start. A project-file warning is therefore on stderr before a slow compile ends. The last stderr line is a summary that lists the same diagnostics. On success, stdout holds only the path of the published IR.

Pricing.fs:3:9: error fsharp.unsupported_mutable: Mutable bindings and fields are not supported; Morphir values are immutable. Use a new let binding, or a record update, instead of changing a value.
compile failed (source, exit code 1); 1 error(s): error fsharp.unsupported_mutable

JSON report

With --json, stdout holds exactly one JSON report. Logs go to stderr. The examples on this page use the sources of the MCK corpus cases frontend-fsharp-0116 (src/Rates.fs and src/Totals.fs) and frontend-fsharp-0001 (broken/Pricing.fs). output and sources.root show example paths.

$ dotnet-morphir compile --language fsharp --input src/Rates.fs src/Totals.fs --package-name acme/demo --json
{
  "success": true,
  "exitCode": 0,
  "language": "fsharp",
  "frontend": "morphir-dotnet.fsharp (in-process)",
  "irVersion": "4",
  "sources": {
    "projectFile": null,
    "root": "src",
    "documents": [
      "Rates.fs",
      "Totals.fs"
    ]
  },
  "modules": [
    "Rates",
    "Totals"
  ],
  "output": "/work/.morphir/out/compile.dest/morphir-ir.json",
  "installed": [],
  "diagnostics": [],
  "failureKind": null,
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "default"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  }
}

The report members are:

MemberMeaning
success, exitCodeThe outcome.
language, frontendThe language and the selected frontend, when known.
irVersionThe requested IR version, exactly as written.
sourcesThe project file (or null), the root, and the documents in compilation order. The root and the project file are relative to the invocation directory when they are in it, else absolute. Each document is relative to the root, with / separators.
modulesThe compiled modules, in compilation order.
outputThe published IR path, or null.
installedThe absolute files that the -o install of this run holds. Empty without -o.
diagnosticsEvery diagnostic of the run, ordered by stage, then document, then position.
failureKindsource, invocation, configuration, operational or cancelled, or null.
originsWhere each setting came from: cli with its flag, configuration with its file and key, or default.
generationOnly with --target: target, backend (null when selection failed), output, and the generate report members sourceVersion, family, distributionKind, representation, artifacts, written, unchanged and removed, and formatting when the backend reports it.

Diagnostics have the frontend (MEP) shape: severity, code, message, and optional location (a document uri and a zero-based range in UTF-16 code units), related and data. A location uri is relative to sources.root, as the documents are.

$ dotnet-morphir compile --language fsharp --input broken/Pricing.fs --package-name acme/demo --json
{
  "success": false,
  "exitCode": 1,
  "language": "fsharp",
  "frontend": "morphir-dotnet.fsharp (in-process)",
  "irVersion": "4",
  "sources": {
    "projectFile": null,
    "root": "broken",
    "documents": [
      "Pricing.fs"
    ]
  },
  "modules": [],
  "output": null,
  "installed": [],
  "diagnostics": [
    {
      "severity": "error",
      "code": "fsharp.unsupported_mutable",
      "message": "Mutable bindings and fields are not supported; Morphir values are immutable. Use a new let binding, or a record update, instead of changing a value.",
      "location": {
        "uri": "Pricing.fs",
        "range": {
          "start": {
            "line": 2,
            "character": 8
          },
          "end": {
            "line": 2,
            "character": 15
          }
        }
      },
      "data": {
        "construct": "mutable binding or field"
      }
    }
  ],
  "failureKind": "source",
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "default"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  }
}

A pre-release that no writer declares fails before compilation:

$ dotnet-morphir compile --language fsharp --input src/Rates.fs --package-name acme/demo --ir-version 4.1.0-draft.1 --json
{
  "success": false,
  "exitCode": 2,
  "language": "fsharp",
  "frontend": null,
  "irVersion": "4.1.0-draft.1",
  "sources": null,
  "modules": [],
  "output": null,
  "installed": [],
  "diagnostics": [
    {
      "severity": "error",
      "code": "compile.unsupported_ir_version",
      "message": "No IR writer declares format version 4.1.0-draft.1. Declared versions: 1, 2, 3, 4."
    }
  ],
  "failureKind": "configuration",
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "cli",
      "flag": "--ir-version"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  }
}

The Native AOT executable has no F# frontend:

$ morphir compile --language fsharp --input src/Rates.fs --package-name acme/demo --json
{
  "success": false,
  "exitCode": 2,
  "language": "fsharp",
  "frontend": null,
  "irVersion": "4",
  "sources": null,
  "modules": [],
  "output": null,
  "installed": [],
  "diagnostics": [
    {
      "severity": "error",
      "code": "compile.frontend_unavailable",
      "message": "No registered frontend reads the language 'fsharp'. This build registers no frontend. The self-contained and framework-dependent morphir and dotnet-morphir have a built-in F# frontend; this offering uses the F# frontend process delivery morphir-fsharp-frontend. Set [frontend.fsharp].extension in morphir.toml, or use --extension, to configure a frontend extension."
    }
  ],
  "failureKind": "configuration",
  "origins": {
    "language": {
      "kind": "cli",
      "flag": "--language"
    },
    "packageName": {
      "kind": "cli",
      "flag": "--package-name"
    },
    "irVersion": {
      "kind": "default"
    },
    "input": {
      "kind": "cli",
      "flag": "--input"
    }
  }
}

JSON Lines

With --json-lines, stdout holds only newline-delimited JSON. Each line is one compact UTF-8 JSON object with an event member. The command writes and flushes each line when its event occurs. Logs stay on stderr.

eventOther membersMeaning
stageStartedstageA stage starts.
diagnosticsstage, optional document, diagnosticsThe diagnostics of a stage for one document, or for no document.
stageCompletedstage, passedA stage completes.
completedreportThe last line, exactly once. report is the report that --json writes.

The stages are settings, sources, capture, frontend (which builds the requested IR family), write and publish, and with --target also generate (model loading, backend selection, option validation and generation) and generatePublish (owned publication of the generated files). A stage that does not run has no records. All diagnostics records together hold the same diagnostics as the report. The records keep emission order; the report keeps stage, document and position order. A parse error, the --json and --json-lines conflict, and cancellation also end with one completed record. Ignore event types and members that you do not know: new ones can be added without a new format version.

$ dotnet-morphir compile --language fsharp --input src/Rates.fs src/Totals.fs --package-name acme/demo --ir-version 3 --json-lines
{"event":"stageStarted","stage":"settings"}
{"event":"stageCompleted","stage":"settings","passed":true}
{"event":"stageStarted","stage":"sources"}
{"event":"stageCompleted","stage":"sources","passed":true}
{"event":"stageStarted","stage":"capture"}
{"event":"stageCompleted","stage":"capture","passed":true}
{"event":"stageStarted","stage":"frontend"}
{"event":"stageCompleted","stage":"frontend","passed":true}
{"event":"stageStarted","stage":"write"}
{"event":"stageCompleted","stage":"write","passed":true}
{"event":"stageStarted","stage":"publish"}
{"event":"stageCompleted","stage":"publish","passed":true}
{"event":"completed","report":{"success":true,"exitCode":0,"language":"fsharp","frontend":"morphir-dotnet.fsharp (in-process)","irVersion":"3","sources":{"projectFile":null,"root":"src","documents":["Rates.fs","Totals.fs"]},"modules":["Rates","Totals"],"output":"/work/.morphir/out/compile.dest/morphir-ir.json","installed":[],"diagnostics":[],"failureKind":null,"origins":{"language":{"kind":"cli","flag":"--language"},"packageName":{"kind":"cli","flag":"--package-name"},"irVersion":{"kind":"cli","flag":"--ir-version"},"input":{"kind":"cli","flag":"--input"}}}}

Difference from the Rust CLI

The Rust morphir CLI (finos/morphir) writes one compact report line for --json-lines. This CLI writes the event stream, one record per line, as the events occur. Until upstream aligns, the two CLIs give different --json-lines output. The last line of this CLI is a completed record that holds the full report, so a consumer that reads only the last line and its report member works with this CLI. An upstream issue proposes this record format as the shared meaning.

Unimplemented options

These fail with exit code 2 and compile.unsupported_setting, which names the option or the key and its file: --types-only, ir.format = "yaml" or "ion", and ir.layout = "document-tree".

See also

15 - Extension installation status

What the extension installer currently implements and what remains

Extension installation status

extension install supports an explicit local-developer release declaration and archive from a registered repository name, with --no-probe consent and a mutation-free --dry-run. The obsolete positional URL/version form is a usage error. Selection and removal commands remain stubs. The management reference gives the command syntax. This does not grant publisher authentication or claim published-release acquisition.

Historical implementation evidence

The following snapshots retain their original source identities and limitations; they do not describe the current command surface.

The current library increment on ad24ce68 can install a release-bound local ZIP into its canonical package tree, register it through the published catalog/lock transaction, and capture immutable selected records with an owned package lease. Local no-probe installation is explicitly named InstallLocalUncheckedAsync and stays labelled local-developer/unchecked. Supported interrupted journals can be recovered with explicit consent; old formats are not converted.

Unpublished snapshots remain expected-current inventory. They cannot authorize a new acquisition after current metadata changes. Retained-generation acquisition requires a supported published binding, which is not implemented by this increment. No new CLI command or generation metadata format is provided here.

The review correction refuses a foreign process-platform activation before it returns ownership or registers local metadata. Windows x64 emulation remains supported. Malformed compressed ZIP content returns a typed archive refusal. Contended state/repository operations return asynchronously and cancellation waits for native lock ownership to settle rather than abandoning a waiter.

Fresh Windows installer assembly runs before and after formatting each pass 285 executed tests, with seven Unix controls ignored and no failures or errors. These include transaction interruptions, journal replacement, empty ZIP directories, exact current-export refusal, native cancellation and real cross-process leases. The existing C# consumer also passes its 27-line JIT run; it does not cover the new package APIs or establish AOT support. Tar packages, repair, full corpus checks, commands and the actual process-provider journey remain unfinished. Independent security/interop re-review, package C# consumer coverage, Unix runtime and Native AOT qualification are still gates. Parent composition contracts are merged at d9f309a1; their runtime binding remains unimplemented here. The earlier 194-test snapshot and evidence below keep their original source identities. Independent review requested security corrections to installation I/O. The fixes now pass 131 executed Windows tests; three Unix-only interleavings were explicitly ignored. Independent security/interop re-review of b6936ba5 found no concrete remaining finding under the approved cooperating Home boundary. Unix runtime evidence and Native AOT remain qualification gates. These results do not qualify a released installer.

Subsequent macOS CI at 4c908047 ran 180 tests, with 177 passing, two failing and one error in file permissions/reopening and temporary-directory disposal. A Darwin ARM64 native-call correction and permission regressions are now under review. Focused Windows verification passes Store 15/15 and RunCopies 5/5, with three Unix-only tests ignored and Unix permission assertions unexecuted. The correction subsequently passed the installer assembly on hosted macOS and Ubuntu at exact head 749dd2fe: 182/182 each, with zero ignored, failed or errored. That closes the ABI platform-evidence gap. Native AOT, acquired artifacts and the full installation lifecycle remain unqualified.

Private streaming stages are the next test-first library draft. After separate missing-API and setup diagnostics, a fresh build succeeded and 16 tests ran. All 16 errored at the declaration stub’s intentional refusal; zero passed or were ignored. No staging mechanism executed, and no behavior or public API approval is established by that earlier stub run. The Begin/Write/Seal implementation now passes 25/25 focused Windows tests that execute byte ownership, integrity, interrupted-write and concurrency controls, including cancellation while waiting for creation admission. Affected Store 15/15 and RunCopies 5/5 pass, with three Unix-only cases ignored. The public C# JIT probe also passes its original 15 output cases plus nine staging cases, including ownership, nullable cleanup, refusal and cancellation controls. Hosted macOS and Ubuntu at 98df4035 then each reported 207 executed, 182 passed and all 25 staging tests errored, with zero ignored or failed. Begin incorrectly declared a raw Unix file handle asynchronous. A source correction preserves the open file and Windows OVERLAPPED behavior while using the Unix handle’s actual mode. Focused Windows verification passes 25 staging, 15 Store and five RunCopies tests before and after formatting, with three Unix-only tests ignored each time. These Windows results leave the hosted Unix failure open; Unix staging GREEN and AOT evidence remain pending. The reviewed hashing dependency was merged normally; publication, host mapping, manager integration and later safe orphan collection remain unfinished. This adds no working installation command.

The next increment drafts tests for publishing sealed private bytes through the existing single-file store. Declaration-only refusal stubs now compile; a focused Windows run executed 17 cases, all errored, with four Unix-only cases ignored. No publication mechanism ran in that stub check. A source implementation now connects sealed ownership to the same store publisher/verifier as the path API. Two fresh focused Windows builds succeed, each with four existing dependency warnings and zero errors. Before and after pinned formatting, the permitted lists each pass 17 sealed-publication, 25 staging, 15 Store and five RunCopies tests. Seven Unix-only cases are ignored in each run. No working command, current Unix qualification or AOT qualification follows from these checks. Independent review found no finding within this sealed-publication scope. The public C# JIT consumer subsequently passes with 27 output lines, preserving its original 24 and adding checks for published bytes/digest/length, nullable cleanup and one-time sealed ownership. Its fresh build has four existing dependency warnings and zero errors; the run has empty stderr. This remains Windows development evidence. Required final verification includes the actual published bytes and Unix mode, including a macOS clone. Orphan collection, host mapping and acquisition lifecycle integration remain unfinished. Prior Windows results do not qualify the newly merged source or replace hosted Unix evidence. The coordinator separately verified the earlier 56665f6c installer suite on hosted macOS and Ubuntu, 207/207 each with zero ignored, failed or errored. That evidence belongs to that exact earlier source.

The installation library can verify and publish a staged single file to a content-addressed store, validate ZIP and tar-gzip archives without extracting them, and clean stale temporary directories owned by this host. It checks a single file against its recorded SHA-256 while copying and rehashes a stored file before reuse. A mismatched stored object is refused and left untouched. Archive inspection checks names, entry types and size, count and expansion limits before anything can be published.

Catalog and lock integration, package extraction, install-time capability probes, activation and installation commands remain unfinished. Shared package formats will use the parent contract and corpus published in finos/morphir#1156 at 0439c4c0. Portable pins and generation-overlay document formats still wait on the separate parent toolchain contract.

The first internal shared-reader increment passes 19 focused Windows tests using data pinned to that parent commit, including six release/schema cases and two I-JSON cases. It is not connected to installation commands. The remaining parent case operations and full claim/package validation are still being implemented.

The internal claims reader additionally passes 27 focused Windows tests for current-schema claims, recursive metadata/comparison and host requirements. It shares embedded parsing with the descriptor reader and uses the existing version comparator library. This reads claims supplied in metadata; install-time probing and publisher authentication remain separate, unfinished integrations.

The maintainer approved project/workspace pins resolved from local inventory and MORPHIR_EXTENSION_OVERLAY above project pins. Explicit frontend extension_id and extensions table id fields will select installed identities without changing existing path-string meaning. CMD-1 keeps remove as an uninstall alias and --platform only on extension list. These commands and selection integrations are not implemented by this approval.

Checksums establish integrity, not who published an extension. A capability check is a separate fact too. Naming a local source or explicitly installing staged files is developer consent, and the result stays labelled local-developer. Automatic unsigned staged installation requires an explicitly configured auto setting; inheriting historical default auto from an omitted setting is not consent. The schema default remains unchanged, and staging integration/tests are still pending. No library operation in this increment authenticates a publisher.

Morphir Home is user-owned. Supported writers must honor stable locks and detected tampering must be refused. This model does not isolate an arbitrary malicious process running as the same user; a separate broker or sandbox is not promised.

Ordinary compilation and generation must not acquire extensions from the network. Staged-file policy will remain explicit. Later commands that change installation state will support --dry-run before writing anything.

The technical library notes describe the current APIs, store layout, archive bounds and platform verification limits. Native AOT qualification and Linux/macOS filesystem runs remain pending.

16 - Generate

Generate target artifacts from a Morphir model

Generate

Generate target files from a decoded Morphir library. Targets fsharp and csharp support classic and v4 models. Target and output are explicit. The input is an explicit --input, or else the IR of the most recent morphir compile of the project.

Synopsis

morphir generate [--input <file-or-directory>] --target <fsharp|csharp> --output <directory>
  [--input-format auto|json|ion] [--option KEY=VALUE ...] [--json]
  [--extension <path-or-command>] [--project <path-or-name>] [--config <file>] [--out-dir <directory>]

Options

OptionMeaning
--inputJSON/Ion model file or Ion tree directory. Without it, the IR of the most recent compile
--targetRequired exact registered target ID, fsharp or csharp
--outputRequired output directory with owned generated files
--input-formatauto by default; json or ion constrains the reader
--optionRepeatable backend option, described below
--jsonWrite one structured report to stdout
--extensionA backend extension: a command name, a path, or a .dll. Overrides [codegen.<target>].extension. See Backend extensions.
--projectThe project directory, workspace member path or project name: its compile record without --input, and its configuration
--configAn explicit configuration file of the project
--out-dirWithout --input: the out root. Overrides MORPHIR_OUT_DIR and workspace.out_dir

Paths resolve against the invocation directory. With --input, no project configuration is required. Without --extension, generate reads the project configuration only for [codegen.<target>].extension. With --project or --config, a configuration error fails the command with exit code 2, as it does for compile. With neither, a discovered configuration that does not load is a warning on stderr, and the built-in backend runs. Repeated option keys use the last value. Values parse as JSON first, so optimize=false is Boolean while optimize='"false"' is a string and is refused by this backend.

generate writes generated files and an ownership manifest. It currently has no --dry-run option; --option format=false controls formatting only.

F# accepts the Boolean options optimize and format, each true by default:

  • optimize runs exact constant folding.
  • format formats the generated source with Fantomas when the host can run it. format=false writes the generator’s own text instead: LF line endings only and exactly one final LF, UTF-8 without a byte order mark, the same bytes on every operating system, under JIT and Native AOT, and in every culture. Use it for output that must not depend on the host, such as the Morphir compatibility check.

format is a backend option, like optimize; it is not --input-format, which says how to read the input. The external F# backend morphir-fsharp-backend accepts the same two options, and they reach it through the options member of morphir.backend.generateModel.

The default formatted text follows the platform’s line endings (CRLF on Windows, LF elsewhere). The Native AOT morphir cannot run Fantomas, so under the default it writes the generator’s own text, says so on stderr, and reports formatting: unavailable. The managed hosts (dotnet-morphir and the managed morphir) format.

C# accepts numeric JSON language-version=14 or language-version=15, default 14, and optional string namespace=Pricing.Generated. Plain namespace text becomes a JSON string through the shared option parser. Numeric strings, preview, latest, unsupported numbers, unknown keys and wrong JSON kinds are invocation errors.

C# namespaces are dotted literal identifiers in Unicode Normalization Form C. An identifier can start with a letter or _ and continue with letters, decimal digits, connector punctuation, combining marks or formatting characters. Reserved keywords require @, for example @class.Models. Literal BMP Unicode is supported, such as Café.模型; source \u/\U escapes and surrogate characters are refused. The core validates this same vocabulary for direct callers. An override changes the input package’s emitted namespace and its qualified references. Relative source paths retain the original package identity.

Inputs and target profile

JSON accepts classic integer versions 1, 2 and 3, their supported semantic release declarations, and v4. Ion text, binary and trees support releases 3.0.0, 3.1.0 and 4.0.0. Automatic detection recognizes the binary marker even without an Ion suffix. An Ion tree has manifest.ion at its root.

The F# target supports Library implementations at exact versions 1, 2, 3, 3.0.0, 3.1.0, 4 and 4.0.0. It emits types, values and mapped SDK operations. Specs/Application, document literals, open records, holes, incomplete/native/ external bodies, nonempty constraints and unknown required semantics are refused. Integers use Int32, floats finite doubles, decimals exact .NET decimal values with retained scale up to 28, and characters one BMP scalar. Unsupported or lossy representations fail before publication.

YAML model files, JSON/YAML trees and MEP envelopes are unsupported. MEP admission is a separate change. Generation produces source files and their ownership manifest. It does not create an F# project or compile and execute the output.

The C# target declares the same exact Library versions. It uses the immutable Morphir.SDK.CSharp runtime and the admitted C# generation contract. Its own semantics and located refusals remain those of the reusable C# library. Generation needs no compiler installation. A separate consumer chooses the matching numeric compiler profile and references the runtime package. The production Roslyn adapter remains deferred.

Backend extensions

A backend extension is a guest process that speaks the Morphir Extension Protocol (MEP) 0.2.0-draft.1 over stdio and serves morphir.backend.generateModel. Select one with --extension, or with [codegen.<target>].extension in the project configuration. The option overrides the key. compile --target does not read the key: it runs the built-in backend and prints a note on stderr when the key is set. Without either, the built-in backend of the target runs, as before. The F# generator is available as the guest morphir-fsharp-backend; see F# generation.

morphir generate --input model.json --target fsharp --output gen --extension morphir-fsharp-backend
[codegen.fsharp]
extension = "morphir-fsharp-backend"

The table is named by the target id. A target id can contain a dot; then the id is one quoted TOML key, [codegen."acme.codegen"]. Without the quotes, [codegen.acme.codegen] is a table codegen inside a table acme, which no target reads.

The command reads the value as compile reads a frontend extension:

  • A name without a directory is searched only in the absolute directories of PATH, in order, and the command starts that absolute path. It is never taken from the invocation directory or from the directory of morphir itself. Starting a bare name directly would look there first: CreateProcess searches the application’s directory and the current directory before PATH on Windows, and .NET does the same on Unix. On Windows the name matches a .exe or .com file, in PATHEXT order, and batch files are not started. On Unix the file must be executable. A name that is not on PATH fails with extension.unavailable; to run a file in the current directory, give a path such as ./guest.
  • A relative path is relative to the invocation directory.
  • A path that ends in .dll runs on the dotnet host: DOTNET_HOST_PATH when it is set, else dotnet searched on PATH in the same way.

What configuration can run. [codegen.<target>].extension names a program that morphir generate starts with your user’s rights. The project configuration is the morphir.toml found from the working directory upward, so running morphir generate inside a checkout that you do not trust can start a program that the checkout chooses: a file inside the checkout, or any command on PATH. MORPHIR_ISOLATED=1 leaves out only the machine and personal configuration files, not the project’s own file, and MORPHIR_DISCOVERY_BOUNDARY only limits how far up the search goes. Read the extension keys of a project before you run morphir generate in it, or choose the file with --config.

generate loads the model first, then starts the guest with explicit arguments and no shell, and passes the guest’s stderr to its own stderr. It agrees the session with initialize and offers JSON IR in the tuples profile morphir-ir, codec morphir-json, envelope 0.1.0-draft.1, IR 4.0.0 and 3.0.0. A v4 model is sent in the 4.0.0 tuple and a classic version 3 model in the 3.0.0 tuple, as the JSON bytes of the input file. Other versions and Ion input are not sent to an extension. The command checks every artifact path that the guest returns, keeps the guest’s artifact order, and publishes the files as for a built-in backend. When the run ends, it sends shutdown and then exit. A guest that does not answer shutdown, or does not take exit, within the deadline (for example a guest that is still busy or no longer reads its input) has its whole process tree stopped. So a request that times out ends within about two deadlines: one for the request and one for the end of the session.

The report names the backend that ran by its own extension id, and adds backendTransport (mep) and backendVersion, the version that the extension gives. A report of a built-in backend has neither member.

An extension failure is exit code 2 with one of these codes, or exit code 1 when the guest rejects the model with error diagnostics:

CodeMeaning
extension.unavailableThe command is not on PATH, the guest did not start, the session did not initialize, the guest does not claim the target, or the guest gave an id reserved for built-ins: every id that starts with morphir. (such as morphir.fsharp), and the id of any registered built-in backend.
extension.tuple_unavailableThe host and the guest agreed no generateModel tuple, or the model needs a tuple that the session did not agree. The message names the tuples.
extension.not_admittedThe guest refused the request, for example an option, or the host did not send the model because it is over the limits that the session agreed (by default 45 MiB decoded, 60 MiB of base64, depth 4096, 50,000,000 nodes).
extension.unsafe_artifactThe guest returned an artifact path that is not a plain relative path; a segment that ends in a period or a space, or is a reserved Windows device name (CON, NUL, COM1 and the like, also with an extension); two paths that differ only in case; or a path that is also the directory of another path. Nothing is published.
extension.protocol_errorThe guest broke the protocol, for example a result that is not a generate result, more than 10,000 artifacts in one result, malformed describe claims, or a -32800 answer that the host did not ask for.
extension.timeoutThe guest did not answer before the deadline.
invalid-extensionThe value of --extension or of the key is empty.

Selection by an installed extension id is not available: the value is a command, a path or a .dll, as for compile.

Input from a prior compile

Without --input, generate reads the IR of the most recent successful morphir compile of the project. It finds the project and the out root in the same way as compile: --project and --config select the project, and the out root is --out-dir, else MORPHIR_OUT_DIR, else the workspace workspace.out_dir, else .morphir/out under the workspace root. generate then reads the compile record compile.json and loads the IR that its ir descriptor names. The report gives that IR file as input and the recorded IR version as sourceVersion. An explicit --input never reads a record.

If there is no compile record, or the most recent compile failed and left a tombstone, generate fails with compile.record_missing and exit code 2, tells you to run morphir compile first, and publishes nothing. If the IR file does not declare the version that the record names, it fails with compile.record_version_mismatch. An --input-format that is not the format of the compiled IR fails with generate.input_format_conflict.

generate holds the shared lock compile.lock from when it reads the record until it has loaded the IR. While a compile of the same project runs, generate writes one waiting line to stderr, waits, and then reads the IR of that compile. A compile that starts while generate loads the IR waits for generate.

With this morphir.toml in the project directory, and the sources src/Rates.fs and src/Totals.fs of the MCK corpus case frontend-fsharp-0116:

[project]
name = "acme/demo"

[frontend]
language = "fsharp"

compile the project, then generate F# from the compiled IR. Run both commands in the project directory. The compile writes the path of the published IR to stdout, and generate writes the generated files:

$ dotnet-morphir compile
/work/demo/.morphir/out/compile.dest/morphir-ir.json
$ dotnet-morphir generate --target fsharp --output gen
Rates.fs
Totals.fs

Examples

Run these from a repository checkout, using its shared fixtures:

morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v3.json --target fsharp --output artifacts/generated-fsharp
morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target fsharp --output artifacts/generated-fsharp --option optimize=false --json
morphir generate --input tests/Fixtures/MorphirIon/pairs/v3-daily-rate.ion --target fsharp --output artifacts/generated-fsharp --input-format ion --json
morphir generate --input tests/Fixtures/MorphirIon/trees/v3-daily-rate --target fsharp --output artifacts/generated-fsharp --json

For C#:

morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v3.json --target csharp --output artifacts/generated-csharp --json
morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target csharp --output artifacts/generated-csharp --option language-version=14 --option namespace=Pricing.Generated --json
morphir generate --input tests/Fixtures/MorphirIon/pairs/v3-daily-rate.ion --target csharp --output artifacts/generated-csharp --input-format ion --json
morphir generate --input tests/Fixtures/MorphirIon/trees/v3-daily-rate --target csharp --output artifacts/generated-csharp --json

--option language-version=15 --option language-version=14 selects 14. Selecting 15 explicitly generates the C# 15 profile independently of the SDK installed on the generation machine. Compile it separately using the matching .NET 11 compiler. See the executable C# walkthrough.

For CI, retain the JSON report and propagate a failure:

morphir generate --input tests/Fixtures/MorphirIR/daily-rate-v4.json --target fsharp --output artifacts/generated-fsharp --json |
    Set-Content artifacts/generation-report.json
if ($LASTEXITCODE -ne 0) { throw "Morphir generation failed" }

Output formats and exit codes

Human output lists relative artifact paths on stdout and diagnostics on stderr. JSON mode writes one document containing success, exitCode, target/backend, input/output, source version/family/kind/representation, ordered artifacts, written, unchanged, removed, diagnostics and failureKind, for a selected backend extension backendTransport and backendVersion, and formatting when the backend reports it: applied (formatted), off (format=false) or unavailable (the host cannot run the formatter). Only a built-in backend reports it. An external backend (backendTransport mep) has no formatting member, even with format=false: a MEP generate result does not say whether the guest formatted, and the host does not report what it did not observe. Logs and warnings use stderr, never stdout. Failed reports contain no successful artifact list. Parsed JSON-mode invocation errors also produce a report.

Structured diagnostics remain fields in that JSON report. JSON failures need not duplicate them as human-readable stderr text. Human-mode diagnostics and any emitted log text use stderr. C# reports identify backend morphir.csharp.

ExitMeaning
0Generation and publication succeeded
1Model or generation semantics are unsupported or invalid
2Invalid invocation, unavailable provider or operational failure
130Cancelled before publication

For a single-module fixture, artifacts contains ["Inventory.fs"]. Its order is the F# compilation order, including on repeat runs when files are unchanged. For the C# daily-rate fixture it contains ["p_Car.Rental/m_InventoryModule.g.cs"]. Both direct generation and the CLI use those same package-derived paths and source bytes.

Publication and ownership

Generated files are published through the shared tree publisher. The output directory contains .morphir-generated.json, an ownership manifest listing artifact paths in declared compilation order. Repeat generation leaves byte-identical files and the manifest untouched. Reports distinguish written, unchanged and removed paths.

Files listed by the previous manifest are removed when no longer generated. Files outside that ownership list remain untouched. Keep the manifest alongside the output so later runs can identify stale owned files.

Input and output paths must be separate. Rooted artifact paths, traversal, case collisions and links are refused before publication. Generation errors preserve previous output. A failure while staging also preserves previous files and the ownership manifest.

Publication does not provide directory-wide rollback. A failure while committing staged files can leave a mix of old and new files. Such failures return an operational error; inspect the output and rerun generation after fixing the reported filesystem problem.

Common errors

Missing settings require an explicit --target and --output. Without --input, generate needs a prior morphir compile of the project. An unknown target lists registered target IDs. Invalid F# options require the Boolean optimize or format, for example optimize=false or format=false. Codec diagnostics identify malformed models and tree members. Limit diagnostics describe the exceeded byte, node, depth, entry or path budget. See CLI troubleshooting.

17 - Validate

Check a Morphir model with the built-in analyzers

Validate

morphir validate reads a Morphir model and runs the enabled analyzers on it. An analyzer reads the model and reports diagnostics. It does not change the model or write files. The command reads the model in the same way as morphir generate.

Synopsis

morphir validate --input <file-or-directory> [--input-format auto|json|ion] [--json]

Options

OptionMeaning
--inputRequired. A JSON or Ion model file, or an Ion tree directory. The path resolves against the invocation directory.
--input-formatauto (the default), json or ion. json and ion select the reader.
--jsonWrite one JSON report to stdout.

The input rules are those of morphir generate: the same readers, the same detection, the same limits and the same refusals (for example YAML input, or a MEP model envelope instead of bare IR).

Analyzers

Both hosts, morphir and dotnet-morphir, include the same built-in analyzers. morphir extension list shows them with the type validator.

IdReadsReports
morphir.type-validatorClassic models, format versions 1, 2 and 3Definite type mismatches in value definitions, with the code type-mismatch

The type validator reports a mismatch only when it is sure: for example, a String literal where the definition declares Int. Types that it does not know stay silent. An IR node has no source position, so the diagnostic has no location, and the message starts with the module and value name.

The run rule:

  • Every enabled analyzer whose IR versions include the model’s format version runs. The diagnostics merge into one result.
  • The result is in a stable order: by extension id, then in the order in which each analyzer reported.
  • When no enabled analyzer reads the model, the result has one info diagnostic with the code no-analyzer that names the family and the format version. The command does not convert the model to another family. Today this is the result for every V4 model.
  • When an analyzer fails, the result names it with the status failed and still holds the diagnostics of the other analyzers.

Output

Without --json, the command writes a table of the diagnostics to stdout, then one line for each analyzer that did not run, then a summary line:

$ morphir validate --input model.json
╭──────────┬───────────────┬──────────┬───────────────────────────────────────────────────────────────────────────────────────────────┬────────────────────────╮
│ SEVERITY │ CODE          │ LOCATION │ MESSAGE                                                                                       │ EXTENSION              │
├──────────┼───────────────┼──────────┼───────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────┤
│ error    │ type-mismatch │          │ Inventory.dailyRate: Value has type Morphir.SDK.String.String, declared                       │ morphir.type-validator │
│          │               │          │ Morphir.SDK.Basics.Int.                                                                       │                        │
╰──────────┴───────────────┴──────────┴───────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────────╯
1 error, 0 warnings, 0 info, 0 hints. Analyzers: 1 ran, 0 not run, 0 failed.

In a terminal the table has styled borders and headers and fits the terminal width. When stdout is redirected, or NO_COLOR is set, the table has no ANSI codes. A location is written as uri:line:character, one-based. When there is no diagnostic, only the summary line is written.

When the command stops before any analyzer runs (an invalid option, or an input that cannot be read), it writes the diagnostics to stderr and nothing to stdout.

With --json, the command writes one report:

$ morphir validate --input model.json --json
{
  "host": "morphir",
  "success": false,
  "exitCode": 1,
  "input": "/work/model.json",
  "family": "Classic",
  "formatVersion": "3",
  "analyzers": [
    {
      "id": "morphir.type-validator",
      "status": "ran"
    }
  ],
  "diagnostics": [
    {
      "severity": "error",
      "code": "type-mismatch",
      "message": "Inventory.dailyRate: Value has type Morphir.SDK.String.String, declared Morphir.SDK.Basics.Int.",
      "extension": "morphir.type-validator"
    }
  ]
}
FieldMeaning
hostThe host that wrote the report: morphir or dotnet-morphir. The two hosts write the same report apart from this field.
successtrue when the exit code is 0.
exitCodeThe exit code of the command.
inputThe full path of the input.
family, formatVersionThe model’s family (Classic or V4) and its declared format version. Left out when the input could not be read.
analyzersEach analyzer, ordered by id, with its status: ran, failed (with a detail) or not-run (with a reason: turned-off or unsupported-model).
diagnosticsThe diagnostics, in the MEP Diagnostic shape: severity (error, warning, info or hint), code, message, location (uri and a zero-based range), related and data, plus extension, the id of the analyzer that reported it. Absent members are left out. A diagnostic of the command itself has no extension.

Logs go to stderr only, so morphir validate --input model.json --json | jq .exitCode works.

The command does not change the model file or the working directory.

Exit codes

CodeMeaning
0No diagnostic has the severity error. Warnings and information are in the result.
1At least one diagnostic has the severity error.
2An invalid invocation, an input or configuration that cannot be read, or an analyzer that failed.
130The command was cancelled.

Configuration

The command reads the layered configuration of the invocation directory, with the same discovery as morphir config. It reads only extensions."<id>".enabled. The value false turns off the analyzer with that id. Extension settings lists every key of the table:

[extensions."morphir.type-validator"]
enabled = false

With this morphir.toml in the project directory, the same command gives:

╭──────────┬─────────────┬──────────┬───────────────────────────────────────────────────────────────────┬───────────╮
│ SEVERITY │ CODE        │ LOCATION │ MESSAGE                                                           │ EXTENSION │
├──────────┼─────────────┼──────────┼───────────────────────────────────────────────────────────────────┼───────────┤
│ info     │ no-analyzer │          │ No enabled analyzer reads the Classic family at format version 3. │           │
╰──────────┴─────────────┴──────────┴───────────────────────────────────────────────────────────────────┴───────────╯
morphir.type-validator: not run (turned-off)
0 errors, 0 warnings, 1 info, 0 hints. Analyzers: 0 ran, 1 not run, 0 failed.

A configuration with an error stops the command with exit code 2 before any analyzer runs.

validate and ir verify

  • morphir ir verify checks that a JSON file has the shape of the Morphir IR JSON schema. Use it to check a file that another tool wrote.
  • morphir validate decodes the model and checks what it means, for example the types of its values. Use it before you generate code from a model.

A model that passes ir verify can still have a type mismatch. A model that validate cannot read gives exit code 2.

External analyzers

Only built-in analyzers run now. External analyzers, delivered as WASM extensions, come in step 2a of the Morphir extension model PRD. They will report the same diagnostic shape and follow the same run rule.