Skip to main content

Morphir configuration merge rules

Purpose

Morphir configuration is layered: multiple configuration sources are loaded and merged to produce one effective configuration. The algorithm operates on parsed configuration values and is independent of whether a source uses TOML or YAML.

This document specifies:

  • Which sources are considered
  • Their precedence order
  • The deterministic merge algorithm used to combine them

Configuration sources and precedence

Sources are loaded from lowest precedence to highest precedence:

PrioritySourceTypical path
1 (lowest)Built-in defaults(compiled in)
2System config/etc/morphir/morphir.toml, or %PROGRAMDATA%\morphir\morphir.toml on Windows
3Global user configPlatform config directory or user-home .morphir directory
4Project configmorphir.toml
5User override.morphir/morphir.user.toml
6 (highest)Environment variablesMORPHIR_*

If the same setting is present in multiple sources, the value from the highest-precedence source wins, subject to the merge algorithm described below.

Each file source accepts a morphir.yaml serialization at the corresponding location (morphir.user.yaml for the user override). A loader MUST accept at most one serialization per location and MUST report an ambiguity error that names both files when a TOML and a YAML file coexist. See the YAML specification for discovery details.

On Windows, %PROGRAMDATA% resolves through the PROGRAMDATA environment variable and falls back to C:\ProgramData when it is unset.

Note: A “hidden project config” variant (.morphir/morphir.toml) may also be used by some commands/workflows. The merge semantics are identical.

When a workspace configuration selects a member project, the member's configuration is merged after the workspace configuration and before the user overrides. The workspace-level .morphir/morphir.user.* file is applied first, then the member's.

Global user path resolution

A loader MUST resolve the platform config directory as follows:

PlatformConfig directory
Linux and other XDG systems$XDG_CONFIG_HOME when it is set to a non-empty absolute path; otherwise $HOME/.config
macOS$XDG_CONFIG_HOME when it is set to a non-empty absolute path; otherwise $HOME/Library/Application Support
WindowsFOLDERID_RoamingAppData, typically %APPDATA%

This follows the XDG Base Directory Specification and the Windows Known Folder API. On an XDG system, a relative XDG_CONFIG_HOME value is invalid and MUST be ignored. The loader then uses the platform default. XDG_CONFIG_DIRS does not define the global user location.

Examples:

EnvironmentResolved YAML candidate
Linux with XDG_CONFIG_HOME=/srv/alice/config/srv/alice/config/morphir/morphir.yaml
Linux with XDG_CONFIG_HOME unset, empty, or relative/home/alice/.config/morphir/morphir.yaml
macOS without XDG_CONFIG_HOME/Users/Alice/Library/Application Support/morphir/morphir.yaml
Windows with Roaming AppData at D:\Profiles\Alice\RoamingD:\Profiles\Alice\Roaming\morphir\morphir.yaml

The standard global user candidates are:

  • <config-directory>/morphir/morphir.toml
  • <config-directory>/morphir/morphir.yaml

The user-home alternatives are:

  • $HOME/.morphir/morphir.toml and $HOME/.morphir/morphir.yaml on Unix-like systems
  • %USERPROFILE%\.morphir\morphir.toml and %USERPROFILE%\.morphir\morphir.yaml on Windows, where the implementation resolves the profile through FOLDERID_Profile

These paths are alternate locations at the same precedence. A loader accepts at most one global user configuration across all candidates. If it finds more than one, it reports an ambiguity error that names every candidate. It MUST NOT merge the files or choose one by path or extension.

Merge algorithm (normative)

Let each configuration source be represented as a nested object (map) map[string]any produced from TOML, YAML, or environment variables.

The effective configuration is computed by applying DeepMerge from low precedence to high precedence:

effective = DeepMerge(
DeepMerge(
DeepMerge(defaults, system),
global
),
project
)
... then merged with user overrides and env vars (if present)

More generally: later maps take precedence over earlier maps.

DeepMerge rules

Given two maps: base and overlay, DeepMerge(base, overlay) produces a new map result and follows these rules:

  • Rule 1 — Overlay wins: for a key present in both maps, the overlay value takes precedence.
  • Rule 2 — Maps merge recursively: if both values for the same key are maps, those maps are recursively deep-merged.
  • Rule 3 — Arrays/slices replace: if values are arrays/slices, the overlay replaces the base entirely (no concatenation).
  • Rule 4 — nil overlay is ignored: if an overlay value is nil, it does not override the base value.
  • Rule 5 — No mutation: the merge result is independent; inputs are not modified.
  • Rule 6 — Secret values are leaves: if the base value, the overlay value, or both are a secret reference ({ env = ... } / { file = ... }) or a secret string, the overlay value replaces the base value entirely; the two are never deep-merged as maps, even when both look like ordinary tables. This rule takes precedence over Rule 2: an ordinary table overlaying a base secret reference (or a secret reference overlaying an ordinary table) still replaces wholesale rather than merging recursively, because merging would otherwise produce a table that is no longer a valid secret reference.

These rules are implemented by deep_merge and merge_all in the morphir_common::config::merge module of morphir-rust. The layered loader in morphir_devkit::config (load_effective_config) applies them across the sources above and records which sources were consulted; morphir config path and morphir config show expose that result.

Provenance

This section is specified for future implementation; it is not implemented today. Nothing named provenance exists in the codebase, and morphir config show has no --provenance flag.

Once implemented, a conforming loader MUST record, for every leaf value and every array in the effective configuration, which source supplied it (the source kind and, for file sources, the path). Provenance MUST follow the winning value through DeepMerge: an overlay value that replaces or is added to the base brings its own provenance with it. A table SHOULD NOT itself carry provenance separate from its children, since a table exists in the effective configuration only because at least one descendant leaf does.

Tooling MAY expose provenance to explain values (for example, a future --provenance option on morphir config show) and to name the file responsible for a validation error. The flag name and output shape are not yet finalized.

Environment variable mapping (informative)

Environment variables are treated as the highest precedence source. Variables starting with the configured prefix (default MORPHIR_) are converted into config keys.

Key mapping:

  • Double underscore (__) indicates nested object boundaries:
    • MORPHIR_CODEGEN__GO__PACKAGE=foocodegen.go.package = "foo"
  • Single underscores are not split into nested keys by the loader; they remain part of the key name at that level:
    • MORPHIR_IR_FORMAT_VERSION=3ir_format_version = 3 (as a single key in the env-derived map)
  • Key segments are lower-cased. Underscores immediately after the prefix are ignored, so MORPHIR__IR__STRICT_MODE and MORPHIR_IR__STRICT_MODE map to the same key.

Value mapping:

  • true and false (any case) become booleans.
  • Integers become numbers.
  • A value that starts with [ or { and parses as JSON becomes an array or object.
  • Anything else stays a string.

When a scalar and a nested key conflict (MORPHIR_IR=x together with MORPHIR_IR__STRICT_MODE=true), the shorter path wins and the nested variable is dropped, regardless of environment iteration order.

The env mapping behavior is intentionally mechanical; it does not attempt to “guess” dotted paths. The final effective configuration still follows the same DeepMerge rules.

  • docs/configuration.md (user-facing configuration guide)
  • docs/spec/morphir-toml/morphir-toml-specification.md (format/structure specification)
  • docs/spec/morphir-yaml/morphir-yaml-specification.md (YAML serialization)