This document provides guidance for AI assistants and developers working on the Morphir Go project.
This is a Go port of the Morphir tooling ecosystem. Morphir is a technology-agnostic intermediate representation (IR) for business logic and data models, enabling code generation, documentation, and analysis across multiple target platforms.
When implementing features, refer to these existing Morphir implementations for consistency:
- finos/morphir - Core Morphir project and IR specification
- finos/morphir-elm - Reference implementation in Elm (most mature)
- finos/morphir-jvm - JVM-based implementation
- finos/morphir-scala - Scala implementation
- finos/morphir-dotnet - .NET implementation (contains IR spec and JSON schemas in documentation)
- finos/morphir-rust - Early-stage Rust tooling
The Morphir IR specification and JSON schemas are available in the morphir-dotnet documentation. Always maintain alignment with the official IR specification when implementing features.
This repo uses ADRs to document important architectural decisions and trade-offs.
- ADRs live in docs/adr.
- When making a significant design change (IR modeling, codec/versioning strategy, CLI UX/behavior), add or update an ADR.
- For discriminated union / sum type representation in Go, start with
ADR-0001.
Functional programming is fundamental to this codebase. All code should follow functional programming principles:
- Immutable data structures - Prefer immutable types and avoid mutating state
- Pure functions - Functions should have no side effects when possible
- Separation of concerns - Clearly define I/O boundaries
- Functional composition - Build complex behavior from simple, composable functions
- Domain-driven design alignment - Model the domain using functional patterns
When writing code:
-
Prefer pure functions over impure ones
- Pure functions are easier to test, reason about, and compose
- Isolate side effects to I/O boundaries
-
Return values and errors instead of mutating state
- Functions should return new values rather than modifying inputs
- Use error returns instead of panics where possible
-
Use immutable data structures
- Prefer structs with value semantics
- Avoid global mutable state
- Use functional update patterns (return new instances)
-
Separate I/O from business logic
- Keep business logic pure and testable
- Isolate file system, network, and user interaction to boundaries
-
Functional composition over imperative flow
- Compose small functions into larger behaviors
- Use higher-order functions where appropriate
Write tests before implementation. Follow the TDD cycle:
- Write a failing test
- Write minimal code to make it pass
- Refactor while keeping tests green
Tests should be:
- Fast
- Independent
- Repeatable
- Self-validating
- Timely
Specify behavior before implementation. Use BDD for feature specifications:
- Write feature specifications in clear, domain language
- Define scenarios with Given-When-Then structure
- Ensure tests reflect business requirements
The examples/ directory contains self-describing example projects that serve as both documentation and integration tests. Each example includes a test.yaml file that declares expected behavior.
To add a new example project:
-
Create the example directory:
mkdir -p examples/my-example
-
Add the project configuration (
morphir.tomlormorphir.json):# examples/my-example/morphir.toml [project] name = "MyExample" version = "1.0.0" source_directory = "src" exposed_modules = ["Main"]
-
Create the
test.yamlfile with expectations:# examples/my-example/test.yaml description: Description of what this example demonstrates workspace: loads: true has_root_project: true member_count: 0 root_project: name: MyExample version: "1.0.0" source_directory: src exposed_modules: - Main config_format: toml
-
The example is automatically discovered and tested!
- The discovery-based scenario finds all
examples/*/test.yamlfiles - No need to modify any feature files
- Run tests with
go test ./tests/bdd/...
- The discovery-based scenario finds all
Available test.yaml fields:
| Field | Description |
|---|---|
description |
Human-readable description of the example |
workspace.loads |
Whether the workspace should load successfully |
workspace.has_root_project |
Whether the workspace has a root project |
workspace.member_count |
Number of workspace members expected |
workspace.root_project |
Expectations for the root project |
workspace.members |
Array of expectations for member projects |
Project expectations:
| Field | Description |
|---|---|
name |
Project name |
version |
Project version (optional) |
source_directory |
Source directory path |
module_prefix |
Module prefix (optional) |
exposed_modules |
List of exposed module names |
config_format |
Configuration format (toml or json) |
Testing specific examples:
You can also test specific examples using the Scenario Outline pattern in feature files:
Scenario Outline: Load <example> workspace and verify expectations
Given the example project "<example>"
When I load the example workspace
Then all workspace expectations should pass
Examples:
| example |
| my-example |Granular assertions:
For more specific testing, use individual assertion steps:
Scenario: Verify my example workspace details
Given the example project "my-example"
When I load the example workspace
Then the workspace loading expectation should pass
And the root project expectations should passModel the domain using functional patterns:
- Use algebraic data types where appropriate
- Model domain concepts as immutable types
- Separate domain logic from infrastructure concerns
- Use functional composition to build domain workflows
- Write self-documenting code with clear names
- Keep functions small and focused
- Follow Go conventions and idioms
- Organize code by feature/domain, not by technical layer
Separation of Output Streams:
- stdout - Use for actual command output (data, results, structured output)
- stderr - Use for logging, diagnostics, progress messages, and error messages
This separation allows users to pipe command output while still seeing diagnostic information, and enables proper shell redirection patterns.
// Good: Output to stdout, diagnostics to stderr
func runCommand(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, "Processing...\n") // Diagnostic message
result := processData(args)
fmt.Fprintf(os.Stdout, "%s\n", result) // Actual output
return nil
}
// Avoid: Mixing output streams
func runCommand(cmd *cobra.Command, args []string) error {
fmt.Println("Processing...") // Goes to stdout - wrong!
fmt.Println(result) // Actual output
return nil
}All non-interactive commands should support a --json flag to output results in JSON format. This enables:
- Machine-readable output for scripting and automation
- Integration with other tools and pipelines
- Consistent structured output across commands
Implementation Pattern:
var jsonOutput bool
func init() {
validateCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output results as JSON")
}
func runValidate(cmd *cobra.Command, args []string) error {
result := validateIR(args)
if jsonOutput {
// Output JSON to stdout
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(result)
}
// Output human-readable format to stdout
fmt.Fprintf(os.Stdout, "%s\n", formatHumanReadable(result))
return nil
}Guidelines:
- JSON output should be written to stdout (not stderr)
- Logging and diagnostics should still go to stderr even when
--jsonis used - JSON output should be well-structured and follow consistent schemas
- When
--jsonis used, avoid mixing JSON with human-readable text - Use proper JSON encoding with indentation for readability (when appropriate)
Interactive Commands:
- Commands that launch interactive UIs (like the root TUI) do not need
--jsonsupport - Commands that can be both interactive and non-interactive should support
--jsonfor non-interactive mode
-
Follow functional programming patterns
- Avoid mutable state
- Prefer pure functions
- Use functional composition
-
Write tests first (TDD)
- Start with failing tests
- Implement to make tests pass
- Refactor with confidence
-
Use BDD for feature specifications
- Define behavior clearly
- Write scenarios that reflect requirements
-
Reference other Morphir implementations
- Check how similar features are implemented in other languages
- Maintain consistency with Morphir IR specification
- Learn from reference implementations (especially morphir-elm)
-
Maintain alignment with Morphir IR specification
- Ensure compatibility with the official IR
- Validate against JSON schemas when available
- Test interoperability with other Morphir tools
-
Follow CLI development guidelines
- Separate stdout (output) from stderr (logging/diagnostics)
- Add
--jsonflag support to all non-interactive commands - Ensure JSON output is well-structured and consistent
IMPORTANT: When AI assistants (like Claude) create commits, DO NOT include Claude as a co-author.
This project is part of the FINOS foundation and uses EasyCLA for Contributor License Agreement management. Adding AI assistants as co-authors breaks the CLA verification process.
Correct approach:
git commit -m "feat: add new feature
This implements the new feature as requested."INCORRECT approach (will break EasyCLA):
git commit -m "feat: add new feature
This implements the new feature as requested.
Co-Authored-By: Claude <noreply@anthropic.com>"For AI assistants generating commits:
- Only include the actual human contributor as the author
- Do not add yourself as a co-author in the commit message
- Do not add footer notes like "Generated with Claude Code"
- Keep commit messages focused on the technical changes
// Good: Pure function, immutable data
func ProcessModel(model Model) (ProcessedModel, error) {
// Process without mutating input
processed := transform(model)
return processed, nil
}
// Avoid: Mutating input
func ProcessModel(model *Model) error {
// Mutating model - not functional
model.Field = newValue
return nil
}// Good: Return new instance
func UpdateState(state State, value int) State {
return State{
Count: state.Count + value,
// Copy other fields
}
}
// Avoid: Mutating state
func UpdateState(state *State, value int) {
state.Count += value
}cmd/morphir/- CLI application (Cobra + Bubbletea)pkg/models/- Morphir IR model typespkg/tooling/- Utilities and toolspkg/sdk/- SDK for building Morphir applicationspkg/pipeline/- Processing pipelines for IR transformations
Each package is a separate Go module, managed via go.work for development.
- Use
misefor build orchestration and environment management (seemise.toml) - Run
mise run buildto build the CLI - Run
mise run testto run all tests - Run
mise run fmtto format code - Run
mise run lintto run linters
The scripts/ directory contains reusable shell scripts used in build, CI, and development workflows. These scripts are referenced from mise.toml task definitions and can also be used directly.
Available Scripts:
scripts/mod-tidy.sh/scripts/mod-tidy.ps1- Runsgo mod tidyfor all modules in the monoreposcripts/install-dev.sh/scripts/install-dev.ps1- Installs themorphir-devbinary to the Go bin directoryscripts/verify.sh/scripts/verify.ps1- Verifies all modules build successfully
Cross-Platform Support:
- All scripts have both bash (
.sh) and PowerShell (.ps1) versions for cross-platform support - The
mise.tomltasks usescripts/detect-os.shfor proper OS detection (windows, linux, darwin) - Windows: Uses PowerShell scripts (
.ps1) and adds.exeextension to binaries - Unix-like (Linux, macOS): Uses bash scripts (
.sh) and no extension for binaries - The mise tasks automatically select the correct scripts and binary extensions based on detected OS
Guidelines for Scripts:
- Scripts should be executable (
chmod +xfor.shfiles) - Bash scripts: Use
#!/usr/bin/env bashshebang andset -eto exit on errors - PowerShell scripts: Use
$ErrorActionPreference = "Stop"for error handling - Scripts should be idempotent when possible
- Keep scripts focused on a single task
- Use scripts in
mise.tomltasks for complex or multi-step operations - Scripts can be used directly or via
mise runcommands - When adding new scripts, create both
.shand.ps1versions for cross-platform support
Adding New Scripts:
- Place new scripts in the
scripts/directory - Create both
.sh(bash) and.ps1(PowerShell) versions - Make bash scripts executable:
chmod +x scripts/your-script.sh - Reference them in
mise.tomltasks with platform detection - Document their purpose in comments at the top of the script
This project follows Semantic Versioning (SemVer):
- MAJOR.MINOR.PATCH (e.g.,
1.2.3)- MAJOR: Breaking changes to public APIs or behavior
- MINOR: New features, backward compatible
- PATCH: Bug fixes, backward compatible
Pre-release versions can be tagged with suffixes:
v1.0.0-alpha.1- Alpha releasev1.0.0-beta.1- Beta releasev1.0.0-rc.1- Release candidate
All notable changes must be documented in CHANGELOG.md following Keep a Changelog format:
- Changes are grouped under: Added, Changed, Deprecated, Removed, Fixed, Security
- Keep an [Unreleased] section at the top for ongoing work
- When releasing, convert [Unreleased] to [VERSION] - YYYY-MM-DD
- Add a new [Unreleased] section for future changes
Hybrid Approach:
- Manually maintain
CHANGELOG.mdfor notable changes - GoReleaser auto-generates release notes from git commits
- Use conventional commit format for better auto-generated notes
Use Conventional Commits for better changelog generation:
<type>(<scope>): <description>
[optional body]
[optional footer]
Types:
feat:- New feature (triggers MINOR version bump)fix:- Bug fix (triggers PATCH version bump)docs:- Documentation onlystyle:- Code style/formatting (no logic change)refactor:- Code refactoring (no behavior change)perf:- Performance improvementtest:- Adding or updating testschore:- Maintenance tasks, dependenciesci:- CI/CD changes
Breaking changes:
- Add
!after type:feat!:orfix!: - Or add
BREAKING CHANGE:in footer (triggers MAJOR version bump)
Examples:
git commit -m "feat(cli): add validate command for Morphir IR"
git commit -m "fix(models): correct package name parsing"
git commit -m "feat!: change IR structure to match spec v2"Releases are automated via GitHub Actions:
-
Update CHANGELOG.md
- Move changes from
[Unreleased]to new version section - Add release date:
## [X.Y.Z] - YYYY-MM-DD - Add new
[Unreleased]section at top - Update version comparison links at bottom
- Move changes from
-
Commit changelog
git add CHANGELOG.md git commit -m "chore: prepare release vX.Y.Z" -
Create and push tag
git tag -a vX.Y.Z -m "Release X.Y.Z" git push origin main git push origin vX.Y.Z -
GitHub Actions automatically:
- Runs CI checks (format, lint, test, build)
- Builds binaries for all platforms (Linux, macOS, Windows)
- Builds for all architectures (amd64, arm64)
- Generates checksums
- Creates GitHub Release with artifacts
- Generates release notes from git history
-
Manual release trigger (optional):
- Go to GitHub Actions → Release workflow
- Click "Run workflow"
- Enter tag name (e.g.,
v0.1.0)
Before creating a release tag, test locally:
# Validate GoReleaser configuration
mise run goreleaser-check
# Build a snapshot (local test, no publish)
mise run release-snapshot
# Full dry-run (validates everything without publishing)
mise run release-testEach release includes:
- Cross-platform binaries: Linux, macOS, Windows
- Multi-architecture: amd64, arm64
- Compressed archives (
.tar.gzfor Unix,.zipfor Windows) - SHA256 checksums (
checksums.txt) - Auto-generated changelog from commits
- Manual changelog from
CHANGELOG.md
Users can install via:
- Binary download - Download from GitHub Releases
- Go install -
go install github.com/finos/morphir-go/cmd/morphir@vX.Y.Z - Homebrew - (future)
brew install finos/tap/morphir
The CLI embeds version information at build time:
morphir --version
# Output: morphir version 0.1.0 (commit: a1b2c3d, built: 2026-01-01T12:00:00Z)Version variables (set via ldflags):
Version- SemVer version (e.g.,0.1.0)GitCommit- Short commit hashBuildDate- ISO 8601 timestamp
Two main workflows:
-
CI Workflow (
.github/workflows/ci.yml)- Triggers: Push to
main, all PRs - Jobs: format check, lint, test, build matrix
- Ensures code quality before merge
- Triggers: Push to
-
Release Workflow (
.github/workflows/release.yml)- Triggers: Tag push (
v*), manual dispatch - Uses GoReleaser for automated releases
- Creates GitHub Release with all artifacts
- Triggers: Tag push (
When in doubt:
- Check reference implementations (especially morphir-elm)
- Consult Morphir IR specification
- Follow functional programming principles
- Write tests first
- Keep code simple and composable
When ending a work session, you MUST complete ALL steps below. Work is NOT complete until git push succeeds.
MANDATORY WORKFLOW:
- File issues for remaining work - Create issues for anything that needs follow-up
- Run quality gates (if code changed) - Tests, linters, builds
- Update issue status - Close finished work, update in-progress items
- PUSH TO REMOTE - This is MANDATORY:
git pull --rebase bd sync git push git status # MUST show "up to date with origin" - Clean up - Clear stashes, prune remote branches
- Verify - All changes committed AND pushed
- Hand off - Provide context for next session
CRITICAL RULES:
- Work is NOT complete until
git pushsucceeds - NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
This project uses beads_viewer for issue tracking. Issues are stored in .beads/ and tracked in git.
# View issues (launches TUI - avoid in automated sessions)
bv
# CLI commands for agents (use these instead)
bd ready # Show issues ready to work (no blockers)
bd list --status=open # All open issues
bd show <id> # Full issue details with dependencies
bd create --title="..." --type=task --priority=2
bd update <id> --status=in_progress
bd close <id> --reason="Completed"
bd close <id1> <id2> # Close multiple issues at once
bd sync # Commit and push changes- Start: Run
bd readyto find actionable work - Claim: Use
bd update <id> --status=in_progress - Work: Implement the task
- Complete: Use
bd close <id> - Sync: Always run
bd syncat session end
- Dependencies: Issues can block other issues.
bd readyshows only unblocked work. - Priority: P0=critical, P1=high, P2=medium, P3=low, P4=backlog (use numbers, not words)
- Types: task, bug, feature, epic, question, docs
- Blocking:
bd dep add <issue> <depends-on>to add dependencies
Before ending any session, run this checklist:
git status # Check what changed
git add <files> # Stage code changes
bd sync # Commit beads changes
git commit -m "..." # Commit code
bd sync # Commit any new beads changes
git push # Push to remote- Check
bd readyat session start to find available work - Update status as you work (in_progress → closed)
- Create new issues with
bd createwhen you discover tasks - Use descriptive titles and set appropriate priority/type
- Always
bd syncbefore ending session