This document provides guidelines for AI code agents working on the Morphir Python codebase.
Morphir Python is a port of Morphir to Python, providing functional domain modeling capabilities. The project follows functional programming principles while maintaining Pythonic usability.
morphir-python/
├── packages/
│ ├── morphir/ # Core library workspace - IR models, types, pure functions
│ └── morphir-tools/ # morphir-python CLI distribution (depends on morphir)
├── tests/
│ ├── unit/ # Unit tests for morphir package
│ └── tools/ # Unit tests for morphir-python package
└── features/ # BDD feature tests (behave)
The root pyproject.toml is a development workspace. packages/morphir/ publishes
morphir, containing the library and SDK without third-party runtime dependencies.
packages/morphir-tools/ publishes morphir-python, containing the CLI and depending
on morphir. They own separate import namespaces and have independent versions and
release tags (morphir-v* and python-v*). The CLI is the future integration surface
for MEP extension-protocol commands and potential WASM build tooling.
morphir (core library)
- Morphir IR (Intermediate Representation) models, in two versions:
morphir.ir.v3(formats 3.0.0 and 3.1.0; morphir-elm is normative for 3.0.0, finos/morphir for the 3.1.0Specsdistribution) andmorphir.ir.v4(draft upstream;morphir.irre-exports it). ReadformatVersiononly throughmorphir.ir.format_version, convert only throughmorphir.ir.migrate, and keep diagnostic codes inmorphir.ir.diagnostics. See packages/morphir/README.md and their/*andtools/ir-interchangespecs inopenspec/specs/ - Type definitions and type algebra
- Pure functional primitives
- Morphir SDK runtime (
morphir.sdk): Basics, Char, String, List, Dict, Set, Maybe, Result, Tuple, Decimal, Int, Number - Zero CLI or IO dependencies
- Should be usable as a standalone library
morphir-python (CLI/tools, in packages/morphir-tools/)
- Command-line interface (
morphir-python, Typer):ir inspect,ir convert - File I/O operations
- Code generation backends
- Depends on
morphircore library
Immutability by Default
Use frozen dataclasses for all data structures:
from dataclasses import dataclass
@dataclass(frozen=True)
class Name:
"""An immutable name consisting of parts."""
parts: tuple[str, ...]Pure Functions
Functions should be pure - same inputs always produce same outputs with no side effects:
# Good: Pure function
def qualify(module_name: Name, local_name: Name) -> Name:
return Name(parts=module_name.parts + local_name.parts)
# Bad: Side effect
def qualify(module_name: Name, local_name: Name) -> Name:
print(f"Qualifying {local_name}") # Side effect!
return Name(parts=module_name.parts + local_name.parts)Model domains using sum types (unions) and product types (dataclasses):
from dataclasses import dataclass
from typing import Union
# Product type (AND)
@dataclass(frozen=True)
class FunctionType:
argument_type: "Type"
return_type: "Type"
@dataclass(frozen=True)
class RecordType:
fields: tuple[tuple[str, "Type"], ...]
@dataclass(frozen=True)
class UnitType:
pass
# Sum type (OR) - Making illegal states unrepresentable
Type = Union[FunctionType, RecordType, UnitType]Design types so that invalid states cannot be constructed:
# Bad: Allows invalid states
@dataclass
class User:
name: str | None
email: str | None
is_verified: bool # Can be True even if email is None!
# Good: Invalid states are impossible
@dataclass(frozen=True)
class UnverifiedUser:
name: str
email: str
@dataclass(frozen=True)
class VerifiedUser:
name: str
email: str
verified_at: datetime
User = Union[UnverifiedUser, VerifiedUser]All code must have complete type annotations. We use strict mode for both mypy and pyright.
from typing import TypeVar, Callable, Sequence
T = TypeVar("T")
U = TypeVar("U")
def map_list(func: Callable[[T], U], items: Sequence[T]) -> list[U]:
"""Apply a function to each item in a sequence."""
return [func(item) for item in items]Use structural pattern matching for handling sum types:
def type_to_string(t: Type) -> str:
match t:
case FunctionType(arg, ret):
return f"({type_to_string(arg)} -> {type_to_string(ret)})"
case RecordType(fields):
field_strs = [f"{name}: {type_to_string(typ)}" for name, typ in fields]
return f"{{ {', '.join(field_strs)} }}"
case UnitType():
return "()"Use Google-style docstrings:
def process_type(type_def: Type, context: Context) -> Result[ProcessedType, Error]:
"""Process a type definition within a context.
Args:
type_def: The type definition to process.
context: The processing context containing scope information.
Returns:
A Result containing either the processed type or an error.
Raises:
ValueError: If the type definition is malformed.
"""- Use ruff for formatting (configured in pyproject.toml)
- Line length: 88 characters
- Double quotes for strings
- 4-space indentation
Follow red-green-refactor:
- Red: Write a failing test first
- Green: Write minimal code to make it pass
- Refactor: Clean up while keeping tests green
# tests/unit/test_name.py
import pytest
from morphir.ir import Name
class TestName:
def test_from_string_splits_on_separators(self) -> None:
name = Name.from_string("hello.world")
assert name.parts == ("hello", "world")
def test_to_string_joins_with_dot(self) -> None:
name = Name(parts=("hello", "world"))
assert name.to_string() == "hello.world"Write behavior-driven scenarios for user-facing features:
# features/type_checking.feature
Feature: Type Checking
As a developer
I want type checking for my domain models
So that I catch errors at compile time
Scenario: Function type matches argument
Given a function type from Int to String
And a value of type Int
When I apply the function to the value
Then the result type should be String- Minimum 80% coverage required
- All public APIs must have tests
- Edge cases and error conditions must be tested
# Run all checks
mise run check
# Individual commands
mise run lint # Linting
mise run format # Format code
mise run typecheck # Type checking (mypy + pyright)
mise run test # Unit tests
mise run test-bdd # BDD tests
mise run coverage # Coverage report- Create feature branch from main
- Make changes following TDD
- Ensure all checks pass:
mise run check - Submit PR with clear description
- Address review feedback
When adding dependencies:
- morphir package: Should have minimal/zero dependencies
- morphir-python package: Can have CLI and IO dependencies
- Always pin minimum versions in pyproject.toml
Use Result types instead of exceptions where appropriate:
from dataclasses import dataclass
from typing import TypeVar, Union
T = TypeVar("T")
E = TypeVar("E")
@dataclass(frozen=True)
class Ok[T]:
value: T
@dataclass(frozen=True)
class Err[E]:
error: E
Result = Union[Ok[T], Err[E]]- Use
tupleinstead oflistfor immutable sequences - Use
frozensetinstead ofsetfor immutable sets - Consider
__slots__for frequently instantiated classes - Profile before optimizing
- All public APIs must have docstrings
- Keep README.md up to date
- Document non-obvious design decisions in code comments
Work is tracked with beads (bd) in the shared Morphir tracker, not
in a database of this repository. The database lives in finos/morphir and syncs over its refs/dolt/data ref.
.beads/config.yaml points bd at it, so you do not need a finos/morphir checkout.
bd bootstrap # first time on a machine: clones the shared database
bd dolt pull # before you start work
bd dolt push # after you change issuesThe readable JSONL mirror on the beads-sync branch is published from finos/morphir, so pushing the database is
all you need to do here. Run bd prime for the full command reference.
All worktrees use one database: .beads/embeddeddolt in the main checkout. A worktree finds it through the git
common directory, inside or outside the repository, so it needs no .beads/redirect file.
Run bd bootstrap in the main checkout before you open a worktree or start an agent session. Any bd command,
including the SessionStart bd prime hook, makes an empty local database when it finds none. An empty database in
the main checkout stops bd bootstrap ("Database already exists"), and a database in a worktree hides the shared
one.
To repair, move the empty embeddeddolt (and backup) directory aside and run bd bootstrap in the main
checkout again. Use bd count to make sure that a database is empty before you move it.
Issues whose work happens here carry the label repo:morphir-python:
bd ready --label repo:morphir-python # unblocked work
bd list --label repo:morphir-python --all # everything, including closed
bd show <id> --long # description, design, notes and commentsWrite superpowers specs, implementation plans and other documents a person reviews as HTML pages, not Markdown. A long Markdown plan gets skimmed; an HTML page with a table of contents, tables, collapsible sections and highlighted code gets read, questioned and corrected, and its explicit structure lets an agent re-read and edit its own document reliably on every later pass. HTML costs more output tokens per document, so the rule covers specs, plans and reports, not commit messages, issue text or notes.
- Start from docs/templates/agent-document.html. Keep its
<head>as it is, so every document looks the same and the effort goes into content. - Reference styles and scripts; do not embed them. They are files in the project, in
docs/templates/assets/: the document styles, the highlight.js setup and highlight.js 11.12.0 (vendored from@highlightjs/cdn-assets, with its license). A document holds only its content, so an agent that reads it does not read the CSS and JavaScript too. In a copy of the template, change eachassets/prefix to the relative path from the copy todocs/templates/assets/, for example../../docs/templates/assets/from.dev/beads-specs/. Add no<style>element, no inline script, no other script and no external resource. If the files do not load, the page shows unstyled content and plain code blocks. - Put code in
<pre><code class="language-python">(alsojson,toml,bash,diff). - Escape
<,>and&. Give every section anidand list the sections innav.toc. - A plan has one
<section class="task" id="task-N">with an<h3>Task N: title</h3>heading per task. - Files:
.dev/beads-specs/<epic>.htmland.dev/beads-plans/<plan-epic>.html..dev/is gitignored; beads is the record (next section).
OpenSpec changes follow this rule only for the design. The openspec CLI parses proposal.md, tasks.md and the
spec.md files, so those stay Markdown: they are the contract the tool validates and merges, not the review page.
The design is the HTML page for review. The project schema openspec/schemas/html-design/, which
openspec/config.yaml selects, makes each change write design.html from a copy of this template. Its <head>
references the same assets from openspec/changes/<name>/, and the archive guidance in openspec/config.yaml
adds one ../ when the change moves to openspec/changes/archive/. When you change the template's <head>, copy
the change into openspec/schemas/html-design/templates/design.html. The generated openspec-* skills say
design.md; in this repository that file is design.html.
Superpowers specs, implementation plans and execution ledgers are stored in beads, not committed here. This
overrides the superpowers skill defaults for spec, plan and ledger location. OpenSpec changes and specs are
different: they live in openspec/ and are committed. See OpenSpec. Beads still
track them. Each change has an epic bead whose spec id is openspec/changes/<name>, named in the proposal's
## Beads section and in the design's meta line. Each ## N. group in tasks.md is a child task bead, with its
id at the end of the group heading. A requirement that comes from a bead ends with (bead: <id>). The
html-design schema instructions and the archive guidance in openspec/config.yaml say when to create, claim,
close and relink these beads. Sync tracker changes with bd dolt push.
- Commits use Conventional Commits, in plain English, with no AI attribution and no
Co-Authored-Bylines. - Merges, releases and anything irreversible wait for the maintainer's approval unless a beads memory records it
(
bd memories approval).
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 dolt push 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