Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,16 @@ tests/Morphir.Tooling.Tests/
└── invalid-*.json
```

### Test Isolation

Tests run in parallel and must stay hermetic. Use `tests/Morphir.TestSupport` (see [Test Isolation](docs/content/docs/contributing/qa/test-isolation.md)):

- Run every child process with `ChildProcess.RunAsync`: it reads output concurrently, waits for a shared concurrency gate (`MORPHIR_TEST_MAX_PROCESSES`), and kills and awaits the process tree on timeout.
- Confine Morphir children with `TestSandbox.Apply`: fake home and machine folders, `MORPHIR_DISCOVERY_BOUNDARY` and `MORPHIR_ISOLATED=1`. Never read or write the real `~/.morphir`, `%APPDATA%\Morphir` or XDG directories.
- Clean up with `TolerantDirectory.TryDelete`/`TryDeleteAsync`; never let cleanup fail a passing test.
- Never call `Environment.SetEnvironmentVariable` or `Directory.SetCurrentDirectory` in tests.
- Process-heavy test projects add the `MachineTripwire` assembly hook, which fails the run if a test escaped its sandbox.

### TDD Anti-Patterns to Avoid

❌ **Don't write tests after the code**
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.4.0-alpha.1] - 2026-11-06

### Fixed
- The global Morphir home is no longer discovered as a project. Walking up from a directory under the home folder used to find `~/.morphir/morphir.toml` through the `.morphir/` layout and load it as both a global source and the project's primary. It now loads only as a global source ([#494](https://github.com/finos/morphir-dotnet/issues/494))
- A native Windows `MORPHIR_HOME` such as `C:\Users\me\.morphir` works. It was treated as relative and joined onto the current directory, so every `config` command failed with `discovery.read_error` ([#494](https://github.com/finos/morphir-dotnet/issues/494))
- Every Morphir assembly now takes its version from CHANGELOG.md, so assemblies no longer reference each other as `1.0.0.0`. An explicit `-p:Version` or `-p:PackageVersion` still wins, for local and test packages. Previously the packed `dotnet-morphir` tool failed `generate` with `Could not load file or assembly 'Morphir.Models, Version=1.0.0.0'`
- `Morphir.Tooling` and `Morphir` can now be installed from NuGet: releases also publish their dependencies `Morphir.Configuration`, `Morphir.Configuration.IO`, `Morphir.IR.Pipeline`, `Morphir.IR.Pipeline.Plugins`, `Morphir.Ion` and `Morphir.CodeGeneration.FSharp`. A build test now fails if a packed package depends on a Morphir package that is not packed
- GitHub releases now include the `morphir-{rid}-v{version}.tar.gz` archives that the proto plugin downloads, so `proto install morphir` works again
Expand All @@ -28,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **CRITICAL**: Removed contradictory Claude co-author example from AGENTS.md and strengthened CLA compliance warnings in both AGENTS.md and CLAUDE.md (issue #270)

### Added
- `MORPHIR_DISCOVERY_BOUNDARY` and `MORPHIR_ISOLATED` environment variables mirror `--discovery-boundary` and `--isolated` for the `config` commands, and `MORPHIR_DISCOVERY_BOUNDARY` also bounds `project migrate` and `workspace migrate`, which gains `--discovery-boundary`. A flag wins over its variable, which wins over the default. `--isolated` now accepts `true` or `false`, so a command can turn off isolation set in the environment, and `config path --json` reports the effective controls and their sources ([#494](https://github.com/finos/morphir-dotnet/issues/494))
- Shared test sandbox, child-process runner and machine tripwire in `tests/Morphir.TestSupport`, so tests stay off the developer's real Morphir configuration ([#494](https://github.com/finos/morphir-dotnet/issues/494))
- Synchronized both landed C# CLI deltas into the main specifications and archived all nine checked tasks; archive PR landing and coordinator cleanup remain pending ([#481](https://github.com/finos/morphir-dotnet/issues/481), [#482](https://github.com/finos/morphir-dotnet/pull/482)).
- Added exact `csharp` CLI generation with numeric 14/15 profiles, a validated optional namespace and the existing owned publication/reporting flow ([#481](https://github.com/finos/morphir-dotnet/issues/481)).
- Synchronized the landed C# generator's eight requirements and 19 scenarios into the main specification and prepared its 13-task OpenSpec archive; archive follow-up landing and coordinator cleanup remain pending ([#474](https://github.com/finos/morphir-dotnet/issues/474), [#478](https://github.com/finos/morphir-dotnet/pull/478)).
Expand Down Expand Up @@ -70,6 +74,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **CLA compliance guidance** in AGENTS.md with remediation steps for accidental Claude co-author inclusion
- **GitHub issue #270** for F# script to automatically remove Claude co-author from commit history

### Removed
- **BREAKING** (Morphir.Tooling API): `Morphir.Tooling.Configuration.ConfigResolver`, `WorkspaceDiscovery` and `TomlParser` are removed. They walked to the filesystem root and treated any `.git` or `.morphir` directory as a workspace, including the global Morphir home, and could not be contained. No restorable package release ever exposed them. Use `ProjectSystem.Discover` / `ProjectSystem.LoadDetailed` and the shared resolver; replace the legacy CI mode with a named profile selected by `--profiles` or `MORPHIR_PROFILES` ([#494](https://github.com/finos/morphir-dotnet/issues/494))

### Changed
- The `dist`, `tool` and `extension` sub-commands now fail with "not implemented yet", exit code 1 and a link to their tracking issue ([#490](https://github.com/finos/morphir-dotnet/issues/490), [#491](https://github.com/finos/morphir-dotnet/issues/491), [#492](https://github.com/finos/morphir-dotnet/issues/492)). Previously they printed a message and exited 0 without doing anything
- **BREAKING**: The classic `Distribution` has a `Specs` case for IR 3.1.0, and `Distribution.packageDefinition` returns an option because a `Specs` distribution has no definitions
Expand Down
2 changes: 2 additions & 0 deletions Morphir.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
<Project Path="tests/Morphir.IR.Pipeline.Tests/Morphir.IR.Pipeline.Tests.fsproj" />
<Project Path="tests/Morphir.IR.Pipeline.Plugins.Tests/Morphir.IR.Pipeline.Plugins.Tests.fsproj" />
<Project Path="tests/Morphir.Tooling.Tests/Morphir.Tooling.Tests.csproj" />
<Project Path="tests/Morphir.TestSupport/Morphir.TestSupport.csproj" />
<Project Path="tests/Morphir.TestSupport.Tests/Morphir.TestSupport.Tests.csproj" />
<Project Path="tests\Morphir.Core.Tests\Morphir.Core.Tests.csproj" />
<Project Path="tests\Morphir.Ion.Tests\Morphir.Ion.Tests.fsproj" />
<Project Path="tests\Morphir.Models.Tests\Morphir.Models.Tests.fsproj" />
Expand Down
1 change: 1 addition & 0 deletions build/Build.Testing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ void RunTests(string configuration)

var myriadExitCodes = new[]
{
"Morphir.TestSupport.Tests",
"Morphir.SDK.Conformance.Tests",
"Morphir.SDK.CSharp.Tests",
"Morphir.IR.Pipeline.Plugins.Tests",
Expand Down
41 changes: 39 additions & 2 deletions docs/content/docs/cli/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,53 @@ A workspace-root invocation reads the root unless shared selection chooses one d
| `--show-scope` | Include winning source roles with entries |
| `--json` | Emit one JSON report with typed JSON values and structured diagnostics |
| `--config <path>` | Overlay an explicitly selected configuration file |
| `--discovery-boundary <directory>` | Include this directory in discovery and exclude its parents |
| `--discovery-boundary <directory>` | Include this directory in discovery and exclude its parents. Default: `MORPHIR_DISCOVERY_BOUNDARY`, else the current filesystem volume |
| `--profiles <names>` | Comma-separated profile names, repeatable in order |
| `--no-profiles` | Disable profiles; conflicts with `--profiles` |
| `--isolated` | Exclude machine and personal files; retain active profiles and supplied environment values |
| `--isolated [true\|false]` | Exclude machine and personal files; retain active profiles and supplied environment values. Default: `MORPHIR_ISOLATED`, else `false`. `--isolated false` turns off isolation set in the environment |
| `--suppress-warning <code>` | Suppress a supported warning, repeatable |
| `--help` | Show help without configuration discovery or loading |

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

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

### Containment from the environment

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

| Variable | Mirrors | Accepted values |
| --- | --- | --- |
| `MORPHIR_DISCOVERY_BOUNDARY` | `--discovery-boundary` | A directory. A relative path resolves against the current directory. Native Windows drive and UNC paths work with either separator |
| `MORPHIR_ISOLATED` | `--isolated` | `true`, `false`, `1` or `0`, in any case |

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

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

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

`--help` never reads either variable.

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

`config path --json` reports what was in effect:

```json
"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](https://github.com/finos/morphir-dotnet/blob/main/docs/spec/schemas/config-inspection-result.schema.json) is checked against subprocess results in the CLI behavior suite. No F# union or reflection-based serialization is exposed.
Expand All @@ -97,6 +133,7 @@ Every read command emits one object with these fields. The [JSON Schema](https:/
| `diagnostics` | Array of `{code, severity, message, path, key, line, column}` |
| `valueTypes` | Array of `{pointer, kind, encoding}` for canonical types requiring JSON metadata |
| `exitCode` | Integer `0`, `1` or `2` |
| `containment` | `path` only: `{boundary, boundarySource, isolated, isolationSource}`; omitted by other operations |

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

Expand Down
6 changes: 4 additions & 2 deletions docs/content/docs/cli/project-migrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: "Preview and apply a loss-checked migration from classic morphir.js
# project migrate

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

Migrate one classic project configuration to TOML. The command maps classic
Expand All @@ -30,7 +30,9 @@ manifest path.
| `--output path` | Choose a supported TOML primary layout at the same project location |
| `--dry-run` | Return the complete plan without creating or changing files |
| `--json` | Emit one structured JSON result on stdout |
| `--discovery-boundary directory` | Bound discovery to an explicitly selected directory tree |
| `--discovery-boundary directory` | Bound discovery to this directory tree. Default: `MORPHIR_DISCOVERY_BOUNDARY`, else the filesystem root. The current directory and `path` must lie inside it; otherwise the command fails with exit code 2 and a `discovery.boundary` diagnostic before reading anything |

Migration always excludes machine and personal configuration, so `MORPHIR_ISOLATED` does not apply. See [containment from the environment]({{< relref "config.md#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
Expand Down
7 changes: 6 additions & 1 deletion docs/content/docs/cli/workspace-migrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The same operation is available to embedded hosts without invoking this command.
```text
morphir workspace migrate [<directory>]
[--include <pattern>]... [--exclude <pattern>]...
[--dry-run] [--json]
[--dry-run] [--json] [--discovery-boundary <directory>]
```

`<directory>` defaults to the current directory. It must name an existing directory.
Expand All @@ -25,6 +25,10 @@ when it contains no workspace declaration. Migration does not search ancestors f
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]({{< relref "config.md#containment-from-the-environment" >}}).

## Arguments and options

| Control | Meaning |
Expand All @@ -34,6 +38,7 @@ from a member or source directory.
| `--exclude <pattern>` | Exclude candidate project directories; repeatable |
| `--dry-run` | Return the complete verified plan and write nothing |
| `--json` | Write one redacted JSON result to stdout; diagnostics remain on stderr |
| `--discovery-boundary <directory>` | Outer limit the directory must lie within. Default: `MORPHIR_DISCOVERY_BOUNDARY`, else the filesystem root. A directory outside it fails with exit code 2 and a `discovery.boundary` diagnostic, and nothing is written |
| `--help` | Show usage without discovery or migration |

Patterns are relative to the selected root. `*` and `?` match within a path
Expand Down
4 changes: 3 additions & 1 deletion docs/content/docs/configuration/resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ beyond Int64 use `ConfigValue.BigInteger`. Other values remain strings. A
conflicting shorter path wins regardless of environment enumeration order.
Origins identify the winning variable. The reserved controls are `MORPHIR_HOME`,
`MORPHIR_PROFILES`, `MORPHIR_DIAGNOSTICS__SUPPRESS`, `MORPHIR_CONFIG`,
`MORPHIR_CONFIG_FILE`, `MORPHIR_PROJECT`, and `MORPHIR_WORKSPACE`.
`MORPHIR_CONFIG_FILE`, `MORPHIR_PROJECT`, `MORPHIR_WORKSPACE`, and the
containment controls `MORPHIR_DISCOVERY_BOUNDARY` and `MORPHIR_ISOLATED`, which
hosts read and never map into configuration.

Validation uses the pinned canonical schema, including nested task/workflow and
toolchain shapes. Unknown permitted properties remain in the typed document.
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/contributing/qa/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ All development in morphir-dotnet follows TDD:
| Property Tests | FsCheck | Property-based verification |
| Integration Tests | TUnit | Cross-component integration |

### Test Isolation

Tests run in parallel and must not touch the developer's machine. Run child processes through `TestSandbox` and `ChildProcess`, clean up with `TolerantDirectory`, and add the machine tripwire to process-heavy test projects. See [Test Isolation](test-isolation).

### Coverage Requirements

- **Minimum Coverage**: 80% for all new code
Expand Down
Loading
Loading