Skip to content

Commit e28fa2f

Browse files
feat(ir): add IR format 3.0.0 alongside draft IR v4, with migrations and CLI (#32)
* docs(openspec): add add-ir-v3-support change proposal, design, and specs Co-authored-by: Cursor <cursoragent@cursor.com> * feat(ir): add format-version reader, v3 model and codecs, v4 layout, draft v4 codec, migrations Co-authored-by: Cursor <cursoragent@cursor.com> * feat(ir): fix shim re-exports, document dual IR versions, tick tasks Make the compatibility shims re-export transitively imported names from their defining v4 modules so mypy strict accepts them, type v3_to_v4 as always Ok, document the v3 and draft v4 packages, the format-version divergence, and the morphir-tools ir CLI, and run the README IR example and IR doctests in tests. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a16852d commit e28fa2f

95 files changed

Lines changed: 9656 additions & 1339 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.beads/config.yaml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Beads issue tracking for this repository uses the shared Morphir tracker.
2+
# The database lives in finos/morphir (refs/dolt/data), not in this repository.
3+
# `bd bootstrap` clones it; `bd dolt pull` and `bd dolt push` keep it in sync.
4+
sync.remote: "git+https://github.com/finos/morphir.git"
15
sync.branch: "beads-sync"
2-
sync:
3-
remote: "git+https://github.com/finos/morphir.git"

‎AGENTS.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,21 @@ morphir-python/
2222
### Package Responsibilities
2323

2424
**`morphir` (core library)**
25-
- Morphir IR (Intermediate Representation) models
25+
- Morphir IR (Intermediate Representation) models, in two versions:
26+
`morphir.ir.v3` (format 3.0.0; morphir-elm is normative) and `morphir.ir.v4`
27+
(draft upstream; `morphir.ir` re-exports it). Read `formatVersion` only through
28+
`morphir.ir.format_version`, convert only through `morphir.ir.migrate`, and keep
29+
diagnostic codes in `morphir.ir.diagnostics`. See
30+
[packages/morphir/README.md](packages/morphir/README.md#morphir-ir-versions) and
31+
`openspec/changes/add-ir-v3-support/`
2632
- Type definitions and type algebra
2733
- Pure functional primitives
2834
- Morphir SDK runtime (`morphir.sdk`): Basics, Char, String, List, Dict, Set, Maybe, Result, Tuple, Decimal, Int, Number
2935
- Zero CLI or IO dependencies
3036
- Should be usable as a standalone library
3137

3238
**`morphir-tools` (CLI/tools)**
33-
- Command-line interface
39+
- Command-line interface (`morphir-tools`, Typer): `ir inspect`, `ir convert`
3440
- File I/O operations
3541
- Code generation backends
3642
- Depends on `morphir` core library

‎README.md‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,32 @@ uv add morphir-tools
4848

4949
## Quick Start
5050

51+
Morphir IR comes in two first-class versions: format 3.0.0 (`morphir.ir.v3`), which
52+
morphir-elm writes today, and draft IR v4 (`morphir.ir.v4`, also re-exported as
53+
`morphir.ir`). Read a morphir-elm file and migrate it to v4:
54+
5155
```python
52-
from morphir.ir import Type, Value
56+
from pathlib import Path
57+
58+
from morphir.ir.migrate import v3_to_v4
59+
from morphir.ir.v3 import codec
5360

54-
# Example usage will be added as the library develops
61+
library = codec.decode_versioned_distribution(Path("morphir-ir.json").read_text())
62+
v4_library = v3_to_v4(library).value
5563
```
5664

65+
Or from the command line:
66+
67+
```bash
68+
morphir-tools ir inspect morphir-ir.json
69+
morphir-tools ir convert morphir-ir.json --to v4 -o morphir-ir.v4.json
70+
```
71+
72+
See the [morphir package README](packages/morphir/README.md#morphir-ir-versions)
73+
for format-version handling, the draft v4 status, and migrations, and the
74+
[`add-ir-v3-support` change](openspec/changes/add-ir-v3-support/proposal.md) for
75+
the design.
76+
5777
## Morphir SDK
5878

5979
`morphir.sdk` is the Python runtime for the Morphir SDK, the standard library that

‎features/ir_interchange.feature‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Feature: IR interchange
2+
As a Morphir tooling user
3+
I want to inspect and convert versioned morphir-ir.json files
4+
So that I can move models between IR format 3.0.0 and draft IR v4
5+
6+
Scenario: Inspect a morphir-elm format 3 file
7+
Given the golden fixture "elm-simpleValueTree.json"
8+
When I run "ir inspect" on it
9+
Then the command exits with code 0
10+
And the output contains "format: 3.0.0"
11+
And the output contains "kind: Library"
12+
And the output contains "package: morphir/example/app"
13+
14+
Scenario: Upgrade a format 3 file to draft v4 and back
15+
Given the golden fixture "elm-simpleValueTree.json"
16+
When I convert it to "v4"
17+
Then the command exits with code 0
18+
And the converted file declares formatVersion 4
19+
When I inspect the converted file
20+
Then the output contains "draft"
21+
When I convert the converted file to "v3"
22+
Then the command exits with code 0
23+
And the converted file has the same JSON as the fixture
24+
25+
Scenario: Unversioned IR is rejected
26+
Given a file with the JSON '{"distribution": []}'
27+
When I run "ir inspect" on it
28+
Then the command exits with code 1
29+
And the errors contain "missing_format_version"
30+
31+
Scenario: Specs cannot be downgraded
32+
Given a file with the JSON '{"formatVersion": 4, "distribution": {"Specs": {"packageName": "a"}}}'
33+
When I convert it to "v3"
34+
Then the command exits with code 1
35+
And the errors contain "unsupported_v4_downgrade"
36+
And no converted file exists
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Step definitions for the IR interchange CLI."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import tempfile
7+
from pathlib import Path
8+
from typing import TYPE_CHECKING
9+
10+
from behave import given, then, when
11+
from typer.testing import CliRunner
12+
13+
from morphir_tools.cli import app
14+
15+
if TYPE_CHECKING:
16+
from behave.runner import Context
17+
18+
FIXTURES = Path(__file__).resolve().parents[2] / "tests" / "fixtures" / "ir" / "v3"
19+
20+
21+
def _workdir(context: Context) -> Path:
22+
if not hasattr(context, "workdir"):
23+
context.workdir = Path(tempfile.mkdtemp(prefix="morphir-bdd-"))
24+
return context.workdir
25+
26+
27+
def _run(context: Context, *args: str) -> None:
28+
context.result = CliRunner().invoke(app, list(args))
29+
30+
31+
@given('the golden fixture "{name}"')
32+
def step_fixture(context: Context, name: str) -> None:
33+
"""Select a golden format-3 fixture as input."""
34+
context.input = FIXTURES / name
35+
context.fixture = context.input
36+
37+
38+
@given("a file with the JSON '{text}'")
39+
def step_json_file(context: Context, text: str) -> None:
40+
"""Write JSON text to an input file."""
41+
path = _workdir(context) / "input.json"
42+
path.write_text(text)
43+
context.input = path
44+
45+
46+
@when('I run "ir inspect" on it')
47+
def step_inspect(context: Context) -> None:
48+
"""Inspect the input file."""
49+
_run(context, "ir", "inspect", str(context.input))
50+
51+
52+
@when('I convert it to "{target}"')
53+
def step_convert(context: Context, target: str) -> None:
54+
"""Convert the input file to the target version."""
55+
context.output = _workdir(context) / f"converted-{target}.json"
56+
_run(
57+
context,
58+
"ir",
59+
"convert",
60+
str(context.input),
61+
"--to",
62+
target,
63+
"-o",
64+
str(context.output),
65+
)
66+
67+
68+
@when("I inspect the converted file")
69+
def step_inspect_converted(context: Context) -> None:
70+
"""Inspect the converted file."""
71+
_run(context, "ir", "inspect", str(context.output))
72+
73+
74+
@when('I convert the converted file to "{target}"')
75+
def step_convert_converted(context: Context, target: str) -> None:
76+
"""Convert the converted file again."""
77+
context.input = context.output
78+
step_convert(context, target)
79+
80+
81+
@then("the command exits with code {code:d}")
82+
def step_exit_code(context: Context, code: int) -> None:
83+
"""Check the exit code."""
84+
assert context.result.exit_code == code, context.result.output
85+
86+
87+
@then('the output contains "{text}"')
88+
def step_output_contains(context: Context, text: str) -> None:
89+
"""Check standard output."""
90+
assert text in context.result.stdout, context.result.stdout
91+
92+
93+
@then('the errors contain "{text}"')
94+
def step_errors_contain(context: Context, text: str) -> None:
95+
"""Check standard error."""
96+
assert text in context.result.stderr, context.result.stderr
97+
98+
99+
@then("the converted file declares formatVersion {version:d}")
100+
def step_declares_version(context: Context, version: int) -> None:
101+
"""Check the converted file's formatVersion."""
102+
assert json.loads(context.output.read_text())["formatVersion"] == version
103+
104+
105+
@then("the converted file has the same JSON as the fixture")
106+
def step_same_json(context: Context) -> None:
107+
"""Check the converted file matches the fixture JSON."""
108+
assert json.loads(context.output.read_text()) == json.loads(
109+
context.fixture.read_text()
110+
)
111+
112+
113+
@then("no converted file exists")
114+
def step_no_output(context: Context) -> None:
115+
"""Check no output file was written."""
116+
assert not context.output.exists()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: html-design
2+
created: 2026-10-27

0 commit comments

Comments
 (0)