Repository navigation
IR 3.1.0: a v3 Specs distribution and v3 JSON/YAML document trees #978
Replies: 2 comments
|
Implementation plan for the approved spec above. It has 14 tasks in four pull requests: a behaviour-preserving layout refactor, IR 3.1.0 in the classic model, v3 JSON/YAML trees, and the CLI, compatibility-kit, spec and kb changes. Each task is test-first and names the files, the interfaces and the commands that verify it. IR 3.1.0 and v3 Document Trees Implementation PlanGoal: Add IR Architecture: The kit's tree layout ( Tech Stack: Rust 2024 workspace (morphir-rust submodule at Spec: Global Constraints
Review Focus
Part 1 — morphir-rust PR 1: the layout refactor (no behaviour change)Branch: in Task 1: Introduce
|
| Today (read.rs) | After |
|---|---|
read_file(MANIFEST, decode_distribution_manifest_file) (230) |
read_file(MANIFEST, M::decode_manifest) |
Packages::of(&DistributionManifestFile) (142) |
Packages::of(&Envelope) — uses only package and dependencies |
read_module_manifest(.., ExpectedEntries::…) (347, 385) |
read_module_manifest(.., M::role(kind, root)) calling M::decode_module |
definition_listing / specification_listing (673, 690) |
one listing(entries, role) returning Listing<Node<..>>; the wrong style keeps today's shape message text |
load_type_definition / load_type_specification (578, 612) |
load_type(.., role) calling M::decode_type_file; definition_body / specification_body (647, 655) keep their exact "expected a definition/specification file" messages |
assemble (266) |
M::assemble(envelope, packages) |
Keep read_file's spelling-mode wrapper, warning collection (550-558), consumed bookkeeping, stray(), stem_of, shape, recursor unchanged. Then pub fn read_tree(files, profile) -> Result<(IRFile, Vec<Warning>), Diagnostic> { read_tree_with::<V4>(files, profile) } — signature unchanged.
- Step 5: Run the baseline gate
Run the Step 1 commands. Expected: identical counts, all pass. Then cargo clippy -q -p morphir-core -p morphir-common -p morphir-mck-adapter --all-targets -- -D warnings → no output.
- Step 6: Commit
cargo fmt -p morphir-core
git add crates/morphir-core/src/ir/layout
git commit -m "refactor(ir): read document trees through a version model
The layout reader keeps the path, stem, budget, claim and stray-file
rules and asks a TreeModel for everything a file's version decides.
The v4 model wraps today's decoders; nothing a reader sees changes.
Refs https://github.com/finos/morphir/issues/970"Task 2: Move the v4 writer onto TreeModel
Files:
- Modify:
crates/morphir-core/src/ir/layout/model.rs(add writer half) - Modify:
crates/morphir-core/src/ir/layout/v4_model.rs - Modify:
crates/morphir-core/src/ir/layout/write.rs(write_module360,write_tree211,write_manifest_header100,encode584)
Interfaces:
- Produces (used by Task 8):
// added to TreeModel
fn encode_manifest(envelope: &Envelope<Self::Kind, Self::Extra>) -> Result<Self::Doc, Diagnostic>;
fn encode_module(module: &ModuleHeader, role: Role, names: (&[Name], &[Name]), file_names: &[(Name, String)])
-> Result<Self::Doc, Diagnostic>;
fn encode_type_file(name: &Name, node: &Node<Self::TypeDef, Self::TypeSpec>) -> Result<Self::Doc, Diagnostic>;
fn encode_value_file(name: &Name, node: &Node<Self::ValueDef, Self::ValueSpec>) -> Result<Self::Doc, Diagnostic>;
// model.rs
pub(crate) struct ModuleHeader { pub path: Path, pub public: bool, pub doc: Option<String> }
// write.rs, generic core
pub(crate) fn write_module_with<M: TreeModel>(
root: Root, package: &PackageName, header: &ModuleHeader, role: Role,
types: &indexmap::IndexMap<String, Node<M::TypeDef, M::TypeSpec>>,
values: &indexmap::IndexMap<String, Node<M::ValueDef, M::ValueSpec>>,
policy: &TreePolicy,
) -> Result<Vec<(String, String)>, Diagnostic>;- Step 1: Implement the four
encode_*methods onV4by building today'sDistributionManifestFile/ModuleManifestFile/TypeDefinitionFile/ValueDefinitionFileand callingwith_type_encoding(TypeEncoding::Compact, || serde_json::to_value(&file)), exactly asencode(write.rs:584) does.encode_modulebuildsModuleEntries::Names. - Step 2: Rewrite
write_module(360) aswrite_module_with::<M>takingrender: &dyn Fn(&M::Doc) -> String(the JSON and YAML callers pass&|doc| profile.write(doc)): order staysfits→stems_for(types)→stems_for(values)→ module manifest → type files → value files.write_definition_module/write_specification_modulekeep their public signatures and callwrite_module_with::<V4>after converting the v4 module intoModuleHeader+Nodemaps;write_specification_modulekeeps its annotations refusal before any budget check (177). - Step 3:
write_treeandwrite_manifest_headerkeep their signatures and output; they callV4::encode_manifest. - Step 4: Run the Task 1 Step 1 gate plus
cargo test -q -p morphir-mck-adapterandcargo clippyas in Task 1 Step 5. Expected: all pass, unchanged counts. - Step 5: Commit
refactor(ir): write document trees through a version model(same trailer style). - Step 6: Open morphir-rust PR 1 (
refactor(ir): read and write document trees through a version model), test plan = the unchanged counts. Merge only with the user's approval; after merge, confirm morphir-rustmainCI on the merge commit succeeds before starting Part 3.
Part 2 — morphir-rust PR 2: IR 3.1.0 in the classic model
Branch: feat/ir-3-1-specs from morphir-rust origin/main (Part 2 does not depend on Part 1 and may proceed in parallel).
Task 3: Widen the support table to [3.0.0,3.2.0)
Files:
- Modify:
crates/morphir-core/src/format_version/support.rs:10 - Modify:
tests/fixtures/format-version-conformance.json(morphir-rust root; line 3, lines 40-46) - Modify:
crates/morphir-core/src/format_version/mod.rs:77test,crates/morphir-core/src/format_version/serde.rs:121-126test,crates/morphir-common/src/ir_transport/root_probe.rs:1340-1342test
Interfaces: Produces REFERENCE_SUPPORT_TABLE = "[3.0.0,3.2.0),[4.0.0,4.1.0)".
- Step 1: Update the failing expectations first — change the three unit tests so
"3.1.0"isCompatibility::Supported(mod.rs:77 test renamedstring_three_one_zero_is_supported),serde.rsunsupported_minor_is_reported_as_a_minoruses"3.2.0", androot_probe.rsexpects{"formatVersion":"3.2.0"}→unsupported_format_version_minorand adds{"formatVersion":"3.1.0"}→ accepted major 3. In the corpus: line 3"supportTable": "[3.0.0,3.2.0),[4.0.0,4.1.0)"; the "v3 later minor" case (40-46) becomes"3.2.0"; add a case{ "name": "v3.1 release", "input": "3.1.0", "release": "3.1.0", "canonical": "3.1.0", "compatibility": "supported" }using the corpus's existing field names (copy the shape of the neighbouring3.0.0case). - Step 2: Run
cargo test -q -p morphir-core --test format_version_conformance && cargo test -q -p morphir-core --lib format_version && cargo test -q -p morphir-common --test format_version_transport_conformance && cargo test -q -p morphir-common --lib root_probe. Expected: FAIL (table still[3.0.0,3.1.0)). - Step 3: Change
support.rs:10to"[3.0.0,3.2.0),[4.0.0,4.1.0)". - Step 4: Re-run Step 2. Expected: PASS.
- Step 5: Commit
feat(ir)!: accept IR format 3.1.0— body: "The reference support table is [3.0.0,3.2.0),[4.0.0,4.1.0). 3.2.0 still fails with unsupported_format_version_minor."
Task 4: DistributionBody::Specs and version-by-content serialization
Files:
- Modify:
crates/morphir-core/src/ir/classic/distribution.rs(enum 49-55, Serialize 57-73, Deserialize visitor 75-124,DistributionSerialize) - Modify (irrefutable
let DistributionBody::Library(..), make them refuse Specs):crates/morphir-core/src/migration/classic_to_v4.rs:714,crates/morphir-projection/src/normalize/v3.rs:9,crates/morphir-elm-binding/src/backend/decode/classic.rs:28,crates/morphir-runtime/src/lib.rs:212,crates/morphir-python-binding/src/ir/classic/read.rs:26,crates/morphir-gleam-binding/src/version/classic.rs:462,crates/morphir-common/src/ir_transport/ion/mod.rs:261, and the testscrates/morphir-python-binding/tests/ir_versions.rs:74,crates/morphir-runtime/tests/v3_arity.rs:352,377,crates/morphir-gleam-binding/tests/frontend_v3_values.rs:36,crates/morphir-core/tests/classic_ir_loading.rs:37,82,crates/morphir-core/tests/classic_distribution_test.rs:60 - Test:
crates/morphir-core/tests/classic_specs_distribution.rs(new)
Interfaces:
-
Produces:
DistributionBody::Specs(Path, Vec<(Path, PackageSpecification<Attrs>)>, PackageSpecification<Attrs>);impl Distribution { pub fn emitted_format_version(&self) -> serde_json::Value }returning3forLibraryand"3.1.0"forSpecs. -
Step 1: Write the failing test
// crates/morphir-core/tests/classic_specs_distribution.rs
use morphir_core::ir::classic::{Distribution, DistributionBody};
const SPECS: &str = r#"{
"formatVersion": "3.1.0",
"distribution": ["Specs", [["my"], ["pkg"]], [],
{ "modules": [ [ [["basics"]], { "types": [ [["int"], { "doc": "", "value": ["OpaqueTypeSpecification", []] }] ], "values": [], "doc": null } ] ] }]
}"#;
#[test]
fn a_specs_distribution_reads_and_writes_as_3_1_0() {
let file: Distribution = serde_json::from_str(SPECS).unwrap();
assert!(matches!(file.distribution, DistributionBody::Specs(..)));
let written = serde_json::to_value(&file).unwrap();
assert_eq!(written["formatVersion"], "3.1.0");
assert_eq!(written["distribution"][0], "Specs");
assert_eq!(serde_json::from_value::<Distribution>(written).unwrap(), file);
}
#[test]
fn a_library_read_as_3_1_0_is_written_as_3() {
let text = r#"{"formatVersion":"3.1.0","distribution":["Library",[["my"]],[],{"modules":[]}]}"#;
let file: Distribution = serde_json::from_str(text).unwrap();
assert_eq!(serde_json::to_value(&file).unwrap()["formatVersion"], 3);
}
#[test]
fn an_unknown_distribution_tag_names_both_kinds() {
let text = r#"{"formatVersion":3,"distribution":["Application",[["my"]],[],{"modules":[]}]}"#;
let error = serde_json::from_str::<Distribution>(text).unwrap_err().to_string();
assert!(error.contains("Library") && error.contains("Specs"), "{error}");
}- Step 2:
cargo test -q -p morphir-core --test classic_specs_distribution→ FAIL (noSpecs). - Step 3: Implement. Add the variant. Serialize:
Specs(path, deps, spec)writes("Specs", path, deps, spec). Deserialize: tag"Specs" | "specs"reads the same 4-tuple with aPackageSpecification<Attrs>fourth element;unknown_variant(tag, &["Library", "Specs"]). Replace the derivedSerializeonDistributionwith a manual one writingformatVersion=emitted_format_version()thendistribution.format_version: u32stays (major only); Task 3's table already accepts"3.1.0"throughdeserialize_baseline_u32. - Step 4: Fix every irrefutable
letlisted above: replacelet DistributionBody::Library(a, b, c) = x;with
let DistributionBody::Library(a, b, c) = x else {
return Err(/* that function's existing error type */ ...("a v3 Specs distribution has no definitions to <verb>"));
};using each function's own error type and constructor (read each site; e.g. MigrationDiagnostic in classic_to_v4 — Task 5 replaces that one with real Specs migration). In tests, use let … else { panic!("a Library") }.
- Step 5:
cargo test -q -p morphir-core --test classic_specs_distribution→ PASS;cargo check -q --workspace --all-targets→ no errors;cargo clippy -q --workspace --all-targets -- -D warnings→ none. - Step 6: Commit
feat(ir): add a v3 Specs distribution to the classic model.
Task 5: Semantic events and the JSON / YAML codecs carry v3 Specs
Files:
- Modify:
crates/morphir-core/src/traversal/event.rs(DistributionHeader 13-30, ModuleEvent 59) - Modify:
crates/morphir-common/src/ir_transport/semantic.rs(ClassicEventVisitor::begin56,emit_classic_v3107-140,collect273-310,collect_classic_v3313-340) - Modify:
crates/morphir-common/src/ir_transport/single_file.rs(trait 15-27,DistributionBodyVisitor::visit_seq175-205, version checks 79/106/323) - Modify:
crates/morphir-common/src/ir_transport/json.rs(V3JsonEventEncoder::accept235-308) - Modify:
crates/morphir-common/src/ir_transport/migration.rs:110-130(Specs →V4Specs) - Modify:
crates/morphir-core/src/migration/classic_to_v4.rs:714(migrate Specs tov4::Distribution::Specs) - Test:
crates/morphir-common/tests/classic_specs_transport.rs(new)
Interfaces:
- Produces:
// event.rs
DistributionHeader::ClassicV3Specs { package: classic::Path },
ModuleEvent::ClassicV3Specification { path: classic::Path, specification: classic::ModuleSpecification<classic::Attrs> },
// single_file.rs trait ClassicV3ModuleVisitor gains:
fn begin_specs(&mut self, package: &classic::Path, dependencies: &[(classic::Path, classic::PackageSpecification<classic::Attrs>)]) -> Result<(), TransportDiagnostic>;
fn visit_module_specification(&mut self, module: classic::package::ModuleSpecEntry<classic::Attrs>) -> Result<(), TransportDiagnostic>;- Step 1: Write the failing test
// crates/morphir-common/tests/classic_specs_transport.rs
use std::io::Cursor;
use morphir_common::ir_transport::{CodecOptions, EventSink, FormatId, IonCodec, IrCodec, IrVersion, JsonCodec, Layout, TransportDiagnostic, YamlCodec};
use morphir_core::traversal::SemanticEvent;
const SPECS: &str = r#"{"formatVersion":"3.1.0","distribution":["Specs",[["my"],["pkg"]],[[[["morphir"],["s","d","k"]],{"modules":[]}]],{"modules":[[[["basics"]],{"types":[[["int"],{"doc":"","value":["OpaqueTypeSpecification",[]]}]],"values":[],"doc":"Basics."}]]}]}"#;
#[derive(Default)]
struct Collect(Vec<SemanticEvent>);
impl EventSink for Collect {
fn accept(&mut self, event: SemanticEvent) -> Result<(), TransportDiagnostic> { self.0.push(event); Ok(()) }
}
fn options(format: FormatId) -> CodecOptions { CodecOptions::new(IrVersion::V3, Layout::SingleFile, format) }
fn events(codec: &dyn IrCodec, text: &str, format: FormatId) -> Vec<SemanticEvent> {
let mut sink = Collect::default();
codec.decode(&mut Cursor::new(text.as_bytes()), &options(format), &mut sink).unwrap();
sink.0
}
fn text(codec: &dyn IrCodec, events: Vec<SemanticEvent>, format: FormatId) -> String {
let mut out = Vec::new();
let mut sink = codec.encoder(&mut out, &options(format)).unwrap();
for event in events { sink.accept(event).unwrap(); }
sink.finish().unwrap();
drop(sink);
String::from_utf8(out).unwrap()
}
#[test]
fn a_v3_specs_distribution_round_trips_through_json_yaml_and_ion() {
let original = events(&JsonCodec::new(), SPECS, FormatId::json());
for (codec, format) in [
(&JsonCodec::new() as &dyn IrCodec, FormatId::json()),
(&YamlCodec::new() as &dyn IrCodec, FormatId::yaml()),
(&IonCodec::new() as &dyn IrCodec, FormatId::ion()),
] {
let written = text(codec, original.clone(), format.clone());
assert_eq!(events(codec, &written, format.clone()), original, "{format}: {written}");
}
let json = text(&JsonCodec::new(), original, FormatId::json());
assert!(json.contains(r#""formatVersion":"3.1.0""#) && json.contains(r#""Specs""#), "{json}");
}(Task 6 makes the Ion leg pass; until then run with --skip is not allowed — keep the Ion leg and let this test stay red through Task 6, committing Task 5 and Task 6 together if preferred. Recommended: do Task 6 before Step 4's final run.)
- Step 2:
cargo test -q -p morphir-common --test classic_specs_transport→ FAIL. - Step 3: Implement events and codecs.
emit_classic_v3:Librarypath unchanged;Specs(package, deps, spec)emitsBegin(ClassicV3Specs{package}),Dependency(ClassicV3{…})per dep,Module(ClassicV3Specification{path, specification})per module,End. Acceptformat_version == 3for both (major).collect/collect_classic_v3:(V3, ClassicV3Specs{package})collectsClassicV3Specificationmodules intoPackageSpecification; aClassicV3definition module under a Specs header (and the reverse) isevent_error("module_kind_mismatch").single_file.rs:DistributionBodyVisitoraccepts tagspecs(case-insensitive) →visitor.begin_specs(..), then aSpecificationModulesSeedcallingvisit_module_specification;ClassicEventVisitorimplements both new methods by emitting the events above.V3JsonEventEncoder: onClassicV3Specswrite{"formatVersion":"3.1.0","distribution":["Specs",(Library keeps{"formatVersion":3,"distribution":["Library",);ClassicV3Specificationmodules serialize withserde_json::to_writer(&[path, specification]); End closes identically.- YAML: uses
classic::Distributionserde (Task 4) — no change beyond Task 4 if its decode path passes the body through; verify. migration.rs:ClassicV3Specs→V4Specs{format_version: "4.0.0", package},ClassicV3Specification→V4Specification{path, module}viaclassic_to_v4's existing module-spec migration (the one it uses for dependency modules).
- Step 4: Run the Step 2 command → PASS (after Task 6). Run
cargo test -q -p morphir-common --no-fail-fast 2>&1 | grep -E "FAILED|test result: FAILED"→ only the two known cache tests. - Step 5: Commit
feat(ir): carry v3 Specs distributions through events and the JSON and YAML codecs.
Task 6: Ion v3 kind: specs
Files:
- Modify:
crates/morphir-common/src/ir_transport/ion/mod.rs(read_library_header441-470,emit_values180-200,v3_datagram255-300,read_library_modules) - Modify:
crates/morphir-common/src/ir_transport/ion/specs.rs(makeread_module_spec/write_module_specpub(super)) - Test:
crates/morphir-common/tests/ion_codec.rs
Interfaces: Consumes DistributionBody::Specs (Task 4). Produces Ion v3 header kind: specs, formatVersion: "3.1.0"; own modules are top-level module::spec::{ name, types, values, doc }.
- Step 1: Write the failing test
// append to crates/morphir-common/tests/ion_codec.rs
#[test]
fn a_v3_specs_datagram_writes_module_specs_and_3_1_0() {
let text = r#"
morphir::{ formatVersion: "3.1.0", kind: specs, packageName: "my/pkg" }
module::spec::{ name: "basics", types: [ public::spec::opaque::type::{ name: "int" } ] }
morphir_footer::{}
"#;
let events = decode(&IonCodec::new(), text, &v3(FormatId::ion())).unwrap();
let ion = encode(&IonCodec::new(), events.clone(), &v3(FormatId::ion())).unwrap();
assert!(ion.contains("kind: specs") && ion.contains(r#"formatVersion: "3.1.0""#), "{ion}");
assert_eq!(decode(&IonCodec::new(), &ion, &v3(FormatId::ion())).unwrap(), events);
}
#[test]
fn a_v3_library_datagram_still_writes_3_0_0() {
let original = decode(&JsonCodec::new(), V3_MODULE_JSON, &v3(FormatId::json())).unwrap();
let ion = encode(&IonCodec::new(), original, &v3(FormatId::ion())).unwrap();
assert!(ion.contains(r#"formatVersion: "3.0.0""#), "{ion}");
}- Step 2:
cargo test -q -p morphir-common --test ion_codec v3_→ the specs test FAILS. - Step 3: Implement.
read_library_headeracceptskindlibraryorspecs(formatVersion major 3 viaaccept_format_version), returning the kind. Forspecs: the datagram body acceptspackage::spec(dependencies, as today) and top-levelmodule::spec(own modules, viaspecs::read_module_spec), refusespublic|private::def::*, and buildsDistributionBody::Specs.v3_datagrammatchesSpecs: headerkind: specs,formatVersion: "3.1.0", then deps, then each own module viaspecs::write_module_spec, then footer. The Ion tree (ion/tree.rs) already routes amodule::specunderpkg/(Task 5 of the Ion work) — adda_v3_specs_distribution_round_trips_through_an_ion_treetoion_document_tree.rsusing theSPECSJSON from Task 5 andsorted_v3. - Step 4: Run Step 2 plus
--test ion_document_treeand Task 5'sclassic_specs_transport→ PASS. - Step 5: Commit
feat(ir): read and write v3 Specs distributions in Ion. - Step 6: Update morphir-rust
CHANGELOG.md[Unreleased](### Added: IR 3.1.0 — v3 Specs and the widened support table;### Changed: the support-table widening, marked breaking for callers that asserted3.1.0was unsupported); open morphir-rust PR 2; merge on approval; confirmmainCI.
Part 3 — morphir-rust PR 3: v3 JSON and YAML trees
Branch: feat/v3-document-trees from morphir-rust origin/main after Parts 1 and 2 have landed.
Task 7: The v3 tree model
Files:
- Create:
crates/morphir-core/src/ir/layout/v3_model.rs - Modify:
crates/morphir-core/src/ir/layout/mod.rs(re-export the v3 API below) - Test:
crates/morphir-core/tests/layout_v3.rs(new) with fixturescrates/morphir-core/tests/fixtures/ir/classic/v3-with-dependencies.json(copy ofmorphir-common/tests/fixtures/ion/v3-with-dependencies.json)
Interfaces:
- Consumes:
TreeModel,read_tree_with,write_module_with(Tasks 1–2);DistributionBody::Specs(Task 4);migration::classic_to_v4::migrate_name. - Produces:
pub struct V3; // impl TreeModel with Kind = V3Kind, Extra = (), File = classic::Distribution
#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum V3Kind { Library, Specs }
pub const V3_TREE_FORMAT_VERSION: &str = "3.1.0";
pub fn read_tree_v3(files: &Tree, profile: Profile) -> Result<(classic::Distribution, Vec<Warning>), Diagnostic>;
pub fn write_tree_v3(file: &classic::Distribution, policy: &TreePolicy) -> Result<Vec<(String, String)>, Diagnostic>;
pub fn write_v3_definition_module(root: Root, package: &PackageName, module: &classic::ModuleEntry<classic::Attrs, classic::Type<classic::Attrs>>, policy: &TreePolicy) -> Result<Vec<(String, String)>, Diagnostic>;
pub fn write_v3_specification_module(root: Root, package: &PackageName, module: &classic::package::ModuleSpecEntry<classic::Attrs>, policy: &TreePolicy) -> Result<Vec<(String, String)>, Diagnostic>;
pub fn write_v3_manifest(kind: V3Kind, package: &PackageName, dependencies: &[PackageName], policy: &TreePolicy) -> (String, String);
pub enum AnyTree { V3(classic::Distribution), V4(v4::IRFile) }
pub fn read_any_tree(files: &Tree, profile: Profile) -> Result<(AnyTree, Vec<Warning>), Diagnostic>;- Step 1: Write the failing tests
// crates/morphir-core/tests/layout_v3.rs
use morphir_core::ir::classic::{Distribution, DistributionBody};
use morphir_core::ir::layout::{Profile, Tree, TreePolicy, read_tree_v3, read_tree, write_tree_v3};
fn fixture(name: &str) -> Distribution {
let text = match name {
"greeting" => include_str!("fixtures/ir/classic/greeting-example.json"),
_ => include_str!("fixtures/ir/classic/v3-with-dependencies.json"),
};
serde_json::from_str(text).unwrap()
}
fn policy(profile: Profile, budget: u32) -> TreePolicy { TreePolicy { profile, path_budget: budget } }
fn tree(files: Vec<(String, String)>) -> Tree { files.into_iter().collect() }
/// Module members come back in path order, so compare with members sorted by name.
fn sorted(mut file: Distribution) -> Distribution {
let sort_def = |modules: &mut Vec<morphir_core::ir::classic::ModuleEntry<_, _>>| {
modules.sort_by(|a, b| format!("{:?}", a.path).cmp(&format!("{:?}", b.path)));
for module in modules.iter_mut() {
module.definition.value.types.sort_by(|a, b| format!("{:?}", a.0).cmp(&format!("{:?}", b.0)));
module.definition.value.values.sort_by(|a, b| format!("{:?}", a.0).cmp(&format!("{:?}", b.0)));
}
};
if let DistributionBody::Library(_, deps, def) = &mut file.distribution {
sort_def(&mut def.modules);
for (_, spec) in deps.iter_mut() {
for module in spec.modules.iter_mut() {
module.specification.types.sort_by(|a, b| format!("{:?}", a.0).cmp(&format!("{:?}", b.0)));
module.specification.values.sort_by(|a, b| format!("{:?}", a.0).cmp(&format!("{:?}", b.0)));
}
}
}
file
}
#[test]
fn a_v3_library_round_trips_through_json_and_yaml_trees() {
for name in ["greeting", "deps"] {
for profile in [Profile::Json, Profile::Yaml] {
let original = fixture(name);
let files = write_tree_v3(&original, &policy(profile, 4000)).unwrap();
let (read, warnings) = read_tree_v3(&tree(files), profile).unwrap();
assert!(warnings.is_empty());
assert_eq!(sorted(read), sorted(original), "{name} {profile:?}");
}
}
}
#[test]
fn every_v3_tree_file_says_3_1_0() {
let files = write_tree_v3(&fixture("deps"), &policy(Profile::Json, 4000)).unwrap();
for (path, text) in &files {
assert!(text.contains(r#""formatVersion":"3.1.0""#), "{path}: {text}");
}
assert!(files.iter().any(|(path, _)| path == "deps/morphir/_sdk/@/basics/money.type"));
}
#[test]
fn a_cut_stem_is_recorded_and_read_back() {
let original = fixture("deps");
let files = write_tree_v3(&original, &policy(Profile::Json, 64)).unwrap();
assert!(files.iter().any(|(_, text)| text.contains("fileNames")), "{files:#?}");
let (read, _) = read_tree_v3(&tree(files), Profile::Json).unwrap();
assert_eq!(sorted(read), sorted(original));
}
#[test]
fn a_node_file_whose_version_differs_from_the_manifest_is_refused() {
let mut files = tree(write_tree_v3(&fixture("greeting"), &policy(Profile::Json, 4000)).unwrap());
let (path, text) = files.iter().find(|(path, _)| path.ends_with(".type")).map(|(p, t)| (p.clone(), t.clone())).unwrap();
files.insert(path.clone(), text.replace(r#""formatVersion":"3.1.0""#, r#""formatVersion":3"#));
let error = read_tree_v3(&files, Profile::Json).unwrap_err();
assert!(format!("{error:?}").contains(&path), "{error:?}");
}
#[test]
fn a_v4_reader_refuses_a_v3_tree_and_the_reverse() {
let v3 = tree(write_tree_v3(&fixture("greeting"), &policy(Profile::Json, 4000)).unwrap());
assert!(read_tree(&v3, Profile::Json).is_err());
}
#[test]
fn a_dependency_named_like_the_distribution_is_refused() {
let mut files = tree(write_tree_v3(&fixture("deps"), &policy(Profile::Json, 4000)).unwrap());
let manifest = files["manifest"].replace(r#""dependencies":["morphir/SDK"]"#, r#""dependencies":["example"]"#);
files.insert("manifest".into(), manifest);
let moved: Vec<(String, String)> = files.iter()
.filter(|(path, _)| path.starts_with("deps/morphir/_sdk/@/"))
.map(|(path, text)| (path.replace("deps/morphir/_sdk/@/", "deps/example/@/"), text.clone()))
.collect();
files.retain(|path, _| !path.starts_with("deps/"));
files.extend(moved);
assert!(read_tree_v3(&files, Profile::Json).is_err());
}
#[test]
fn a_specs_tree_with_a_definition_file_is_refused() {
let specs: Distribution = serde_json::from_str(r#"{"formatVersion":"3.1.0","distribution":["Specs",[["my"],["pkg"]],[],{"modules":[[[["basics"]],{"types":[[["int"],{"doc":"","value":["OpaqueTypeSpecification",[]]}]],"values":[],"doc":null}]]}]}"#).unwrap();
let mut files = tree(write_tree_v3(&specs, &policy(Profile::Json, 4000)).unwrap());
files.insert("pkg/my/pkg/basics/int.type".into(),
r#"{"formatVersion":"3.1.0","name":"int","def":{"access":"Public","value":{"doc":"","value":["TypeAliasDefinition",[],["Unit",{}]]}}}"#.into());
let error = read_tree_v3(&files, Profile::Json).unwrap_err();
assert!(format!("{error:?}").contains("expected a specification file"), "{error:?}");
}
#[test]
fn a_v3_specs_distribution_round_trips_through_a_tree() {
let specs: Distribution = serde_json::from_str(r#"{"formatVersion":"3.1.0","distribution":["Specs",[["my"],["pkg"]],[],{"modules":[[[["basics"]],{"types":[[["int"],{"doc":"","value":["OpaqueTypeSpecification",[]]}]],"values":[],"doc":"Basics."}]]}]}"#).unwrap();
let files = write_tree_v3(&specs, &policy(Profile::Yaml, 4000)).unwrap();
assert_eq!(read_tree_v3(&tree(files), Profile::Yaml).unwrap().0, specs);
}- Step 2:
cargo test -q -p morphir-core --test layout_v3→ FAIL (API missing). - Step 3: Implement
V3.- Canonical names: classic → canonical with
migrate_name(&name, &IrCursor::root())then.to_canonical_string(); paths join segment names with/; canonical → classic withclassic::Name::new(parsed.words()). decode_manifest: requireformatVersion == "3.1.0"(string; anything else →version_mismatchdiagnostic atmanifest#/formatVersion),distribution ∈ {Library, Specs},package,pathBudget ≥ 64, optionaldependencies; unknown members →UnknownMember(mirror v4's member check);entryPoints→UnknownMember.role:(Library, Pkg) → Definitions, otherwiseSpecifications.decode_module: membersformatVersion(must equal"3.1.0"),path, optionalaccess("Private"only under Definitions; refused under Specifications), optionaldoc(string or array of lines, as v4),types/values(array →Names, object → inline payloads decoded with the node rules below), optionalfileNames(validated against the listed names as v4 does).decode_type_file/decode_value_file: membersformatVersion("3.1.0"),name, exactly one ofdef/spec;defmust match roleDefinitionselse "expected a specification file" (and the reverse message) — reuse the exact texts fromread.rs647-655; payloads decode withserde_json::from_value::<classic::AccessControlled<classic::Documented<classic::TypeDefinition<classic::Attrs>>>>/…ValueDefinition<classic::Attrs, classic::Type<classic::Attrs>>>/classic::Documented<classic::TypeSpecification<classic::Attrs>>/classic::Documented<classic::ValueSpecification<classic::Attrs>>; a serde error becomesDiagnosticat<path>#/defor#/spec.assemble:Library→DistributionBody::Library(package, deps, PackageDefinition{modules})with each moduleModuleEntry{path, definition: AccessControlled{access, value: ModuleDefinition{types, values, doc}}};Specs→DistributionBody::Specs(package, deps, PackageSpecification{modules});format_version: 3. A dependency whose package equals the distribution's own → refuse ("a v3 dependency cannot name the distribution package").- Encoders mirror the decoders; the manifest writes
formatVersion,distribution,package,pathBudget,dependencies(only if non-empty); the module manifest writesformatVersion,path,accessonly when Private,doconly when present,types,values,fileNamesonly when non-empty. - Version agreement: in
read_tree_with, the shared reader compares every file'sPayload::format_version()with the manifest's and reports a mismatch at the file — implement once in the sharedread_filefor both models (v4 trees already agree by construction; the check is a no-op for them — confirm Task 1 gate stays green). read_any_tree: read the manifest JSON'sformatVersion;"3.1.0"(or any3.x) →read_tree_v3, elseread_tree.
- Canonical names: classic → canonical with
- Step 4: Run Step 2 → PASS; the Task 1 gate → unchanged.
- Step 5: Commit
feat(ir): read and write v3 document trees in the JSON and YAML profiles.
Task 8: The document-tree transport accepts v3 for every profile
Files:
- Modify:
crates/morphir-common/src/ir_transport/document_tree.rs(validate_options237-259;KitSink::begin727,dependency812,module861;DocumentTreeSource::open1020-1026) - Test:
crates/morphir-common/tests/document_tree_transport.rs
Interfaces: Consumes Task 7's write_v3_*, read_tree_v3; Task 5's events. Produces: DocumentTreeSink / DocumentTreeSource for (V3, json|yaml).
- Step 1: Write the failing tests (append; reuse the file's
every_physical_path,memory_root,physical_roothelpers)
fn classic_events(text: &str) -> Vec<morphir_core::traversal::SemanticEvent> {
let mut sink = Collect::default();
JsonCodec::new().decode(&mut std::io::Cursor::new(text.as_bytes()),
&CodecOptions::new(IrVersion::V3, Layout::SingleFile, FormatId::json()), &mut sink).unwrap();
sink.0
}
#[derive(Default)]
struct Collect(Vec<morphir_core::traversal::SemanticEvent>);
impl EventSink for Collect {
fn accept(&mut self, event: morphir_core::traversal::SemanticEvent) -> Result<(), TransportDiagnostic> { self.0.push(event); Ok(()) }
}
fn v3_round_trip(root: VfsPath, format: FormatId) {
let original = classic_events(include_str!("fixtures/ion/v3-with-dependencies.json"));
let options = CodecOptions::new(IrVersion::V3, Layout::DocumentTree, format.clone());
let mut sink = DocumentTreeSink::new(root.clone(), options.clone()).unwrap();
for event in original.clone() { sink.accept(event).unwrap(); }
sink.finish().unwrap();
let mut source = morphir_common::ir_transport::DocumentTreeSource::open(root, options).unwrap();
let mut read = Vec::new();
while let Some(event) = source.next_event().unwrap() { read.push(event); }
assert_eq!(read.len(), original.len(), "{format}");
}
#[test] fn a_v3_json_tree_round_trips_on_a_memory_vfs() { v3_round_trip(memory_root(), FormatId::json()); }
#[test] fn a_v3_yaml_tree_round_trips_on_a_physical_vfs() {
let temp = tempfile::tempdir().unwrap();
v3_round_trip(physical_root(temp.path()), FormatId::yaml());
}
#[test]
fn a_v4_source_refuses_a_v3_tree() {
let root = memory_root();
v3_round_trip(root.clone(), FormatId::json());
assert!(read_document_tree_with_options(&root, &options(FormatId::json())).is_err());
}
#[test]
fn a_json_node_file_in_a_yaml_v3_tree_is_refused() {
let root = memory_root();
v3_round_trip(root.clone(), FormatId::yaml());
let yaml = root.join("pkg/example/eligibility/module.yaml").unwrap();
let json = root.join("pkg/example/eligibility/module.json").unwrap();
json.create_file().unwrap().write_all(yaml.read_to_string().unwrap().as_bytes()).unwrap();
yaml.remove_file().unwrap();
let options = CodecOptions::new(IrVersion::V3, Layout::DocumentTree, FormatId::yaml());
assert!(morphir_common::ir_transport::DocumentTreeSource::open(root, options).is_err());
}Also copy the Ion test fixture's semantic comparison: replace the read.len() check with the sorted_v3 JSON comparison from ion_document_tree.rs (move sorted_v3 into a shared tests/common/v3.rs module and mod common; in both files).
- Step 2:
cargo test -q -p morphir-common --test document_tree_transport v3→ FAIL (version_unsupported). - Step 3: Implement.
validate_optionsdrops the "v4 unless Ion" refusal.KitSinkholdsversion: IrVersion; under V3 it acceptsClassicV3Library/ClassicV3Specsheaders,DependencyEvent::ClassicV3(written withwrite_v3_specification_module(Root::Deps, …)per module),ModuleEvent::ClassicV3(write_v3_definition_module(Root::Pkg, …)) andClassicV3Specification(write_v3_specification_module(Root::Pkg, …)), and writeswrite_v3_manifestat End; v4 events under a V3 sink (and the reverse) →event_error("version_mismatch").DocumentTreeSource::openfor kit spellings reads the manifest'sformatVersionfrom the parsed JSON:3.xrequiresoptions.version() == V3and callsread_tree_v3+semantic::emit_classic_v3; otherwise today's v4 path; a mismatch →morphir::ir::detection::version_mismatchwith guidance "select the version the tree's manifest names". - Step 4: Run Step 2 → PASS;
cargo test -q -p morphir-common --test document_tree_transport --test ion_document_tree→ all pass. - Step 5: Commit
feat(ir): stream v3 distributions through JSON and YAML document trees.
Task 9: The MCK adapter reads and writes v3 trees
Files:
-
Modify:
crates/morphir-mck-adapter/src/testee.rs(read_tree_here138-190,write_tree_here195-230) -
Test:
crates/morphir-mck-adapter/tests/protocol.rs -
Step 1: Write the failing test — a
readTreerequest withversion: 3,profile: json, and the files of Task 7'sa_v3_specs_distribution_round_trips_through_a_treemust answer the classic JSON document; awriteTreerequest withversion: 3and that document must answer the same files. Use the file's existing request helpers (read them first; mirror the nearest v4readTreetest's structure, changingversionand the inputs). -
Step 2:
cargo test -q -p morphir-mck-adapter --test protocol v3→ FAIL (version != 4refusal). -
Step 3: Implement.
read_tree_here:version 3→layout::read_tree_v3(&files, layout_profile(profile))and answerserde_json::to_value(&distribution);write_tree_here:version 3→ parse the input asclassic::Distribution(JSON or YAML viamorphir_core::ir::yaml::read+serde_json::from_value) andlayout::write_tree_v3. Other versions keep today's refusal. -
Step 4: Run Step 2 → PASS;
cargo test -q -p morphir-mck-adapter→ all pass. -
Step 5: Commit
feat(mck): read and write v3 document trees in the Rust adapter; morphir-rustCHANGELOG.md[Unreleased]### Addedline for v3 JSON/YAML trees; open morphir-rust PR 3; merge on approval; confirmmainCI.
Part 4 — finos/morphir PR: CLI, compatibility kit, spec, kb
Branch: feat/ir-3-1-v3-trees from finos/morphir origin/main, pinning ecosystem/morphir-rust to PR 3's merge commit. Start from the spec branch: git checkout -b feat/ir-3-1-v3-trees origin/docs/ir-3-1-v3-trees && git rebase origin/main.
Task 10: Pin, support table, and baselines
Files:
-
Modify:
ecosystem/morphir-rust(gitlink),Cargo.lock(ifcargo build -p morphirchanges it) -
Modify:
docs/spec/ir/fixtures/format-version-conformance.json(lines 3, 267-310, 324 — identical edits to Task 3's corpus),docs/spec/ir/format-version.md(lines 39, 82, 91-95),docs/spec/ir/schemas/migration-guide.md:568 -
Modify:
crates/morphir-mck/src/transport/protocol.rs:617,642(test fixtures),crates/morphir-mck/tests/runner_parity.rs:152,website/scripts/test-ir-storage-profiles.js:607,spec/ir/mck/protocol.schema.json:69(example value),spec/mck/baseline/README.md:38 -
Regenerate:
spec/mck/baseline/transcripts/morphir-rust.ndjson,spec/mck/baseline/reports/morphir-rust.json -
Step 1: Pin and build:
git -C ecosystem/morphir-rust checkout <PR3 merge sha> && cargo build -q -p morphir. Thencargo test -q -p morphir-mck 2>&1 | grep -E "FAILED|test result"→ expected FAIL in the tests that compareformatVersionswith[3.0.0,3.1.0)fixtures. -
Step 2: Replace
[3.0.0,3.1.0)with[3.0.0,3.2.0)in every file listed above (text edits; the docs corpus must equal morphir-rust'stests/fixtures/format-version-conformance.jsonbyte for byte —diffthem). Regenerate the baseline with the task the baseline README names (readspec/mck/baseline/README.mdfor the command; do not hand-edit the transcript). -
Step 3:
cargo test -q -p morphir-mck && cargo test -q -p morphir --test mck_run→ PASS. -
Step 4: Commit
feat(ir)!: accept IR format 3.1.0 in the CLI and the kit.
Task 11: The CLI reads and writes v3 JSON and YAML trees
Files:
-
Modify:
crates/morphir/src/commands/migrate.rs:101-112 -
Modify:
crates/morphir/src/commands/migrate/format.rs:49-89(directory branch) -
Modify:
crates/morphir/src/commands/ir_storage.rs:278-304 -
Test:
crates/morphir/tests/migrate_acceptance.rs,crates/morphir/src/commands/migrate/format.rstests,crates/morphir/src/commands/ir_storage.rstests -
Step 1: Write the failing tests — replace
a_v3_json_tree_is_still_refused(migrate_acceptance.rs:425) with:
#[test]
fn a_v3_distribution_round_trips_through_v3_json_and_yaml_trees() {
for format in ["json", "yaml"] {
let temp = TempDir::new().unwrap();
let tree = temp.path().join("model.morphir-dist");
let json = temp.path().join("model.json");
let v3 = ["--target-version", "v3"];
assert_success(&migrate(&greeting_v3(), &tree,
&[&v3[..], &["--output-layout", "vfs", "--output-format", format]].concat()));
let manifest = std::fs::read_to_string(tree.join(format!("manifest.{format}"))).unwrap();
assert!(manifest.contains("3.1.0"), "{manifest}");
assert_success(&migrate(&tree, &json, &[&v3[..], &["--output-layout", "single-file"]].concat()));
let canonical = temp.path().join("canonical.json");
assert_success(&migrate(&greeting_v3(), &canonical, &v3));
assert_eq!(sorted_v3(&json), sorted_v3(&canonical), "{format}");
}
}In format.rs tests add a_v3_json_tree_reports_its_manifest_version (write manifest.json with {"formatVersion":"3.1.0","distribution":"Library","package":"example","pathBudget":4000}, expect IrVersion::V3, Layout::DocumentTree). In ir_storage.rs tests add a_v3_json_tree_is_probed_and_read_as_v3, mirroring a_v3_ion_tree_is_probed_and_read_as_v3 with FormatId::json() and manifest.json.
- Step 2:
cargo test -q -p morphir --test migrate_acceptance v3 && cargo test -q -p morphir --lib "format::tests::a_v3" "ir_storage::tests::a_v3"→ FAIL. - Step 3: Implement.
migrate.rs: delete the v3-tree refusal block.format.rs: for any detected tree format, readmanifest.<ext>(manifest.json,manifest.yaml/manifest.yml,manifest.ion) throughprobeanddetect_version; drop the "JSON and YAML trees are v4 only" comment.ir_storage.rs: theIrLayout::DocumentTree if version == IrVersion::V3branch now serves every format — update its comment to "A v3 tree streams through the JSON encoder, as a single file does." - Step 4: Run Step 2 → PASS;
cargo test -q -p morphir --no-fail-fast→ no failures;cargo clippy -q -p morphir --all-targets -- -D warnings→ none. - Step 5: Commit
feat(cli): write and read v3 JSON and YAML document trees.
Task 12: Compatibility-kit cases
Files:
-
Modify:
spec/ir/mck/document-tree.md(appenddocument-tree-0010…-0016),spec/ir/mck/versions.md(append two cases),spec/ir/mck/distributions.md(append one case) -
Step 1: Write the cases. Every heading carries
version=3. Use the exact fence grammar ofdocument-tree-0008(yaml file path=<logical> set=<name>fences, thenyaml canonical+json canonicalof the whole document):document-tree-0010: v3 manifest {node=DistributionManifestFile version=3}— canonical YAML/JSON of{"formatVersion":"3.1.0","distribution":"Library","package":"my-org/my-project","pathBudget":4000}.document-tree-0011: v3 Library module with definition files {node=Distribution version=3}— set withmanifest,pkg/my-org/my-project/domain/module,pkg/my-org/my-project/domain/user.type(defalias tomorphir/SDK:string#stringspelled as the classic["Reference", {}, [[["morphir"],["s","d","k"]],[["string"]],["string"]], []]), plus the single-file canonical.document-tree-0012: v3 dependency specifications under deps {node=Distribution version=3}— the 0008 shape in v3 (deps/morphir/_sdk/@/basics/int.typewithspec: {doc: "", value: [OpaqueTypeSpecification, []]}).document-tree-0013: v3 Specs tree {node=Distribution version=3}.document-tree-0014: v3 cut stem in fileNames {node=Distribution version=3}— reuse 0004's names and budget 64.document-tree-0015: v3 JSON profile {node=Distribution version=3}— 0011's set asjson filefences.document-tree-0016: a v4 file in a v3 tree is rejected {node=Distribution version=3}— arejectedfence withdiagnostic=version_mismatch(confirm the code name the Rust adapter returns in Task 9's test and use it verbatim).versions-0009: 3.1.0 is supported {node=FormatVersion version=3}andversions-0010: 3.2.0 is a later minor {node=FormatVersion version=3}(rejectedwithdiagnostic=unsupported_format_version_minor).distributions-00NN(next free number):a v3 Specs distribution {node=Distribution version=3}with the Task 4SPECSJSON asjson canonical.
-
Step 2:
./target/debug/morphir mck check spec/ir/mck→ PASS (syntax);mise run mck:run-rust→ the new cases pass for the Rust adapter. -
Step 3: Mark the new cases pending for the TypeScript adapter the way the kit tracks per-adapter gaps (read
spec/ir/mck/README.md"allowed failing" section; add ids tospec/ir/mck/allowed-failing.jsononly if the TypeScript gate runs in CI). -
Step 4: Commit
test(mck): pin v3 document trees and IR 3.1.0.
Task 13: Spec pages, schema, kb, changelog
Files:
-
Create:
docs/spec/ir/schemas/v3/document-tree-files.md -
Modify:
docs/spec/ir/schemas/v3/whats-new.md(new## Version 3.1.0section before## Key Changes from Version 2),docs/spec/ir/schemas/v3/index.md(link),website/static/schemas/morphir-ir-v3.yaml(distributiononeOfLibrary | Specs; description),website/static/schemas/morphir-ir-v3.json(regenerated),docs/spec/ir/schemas/v4/document-tree-files.md(one sentence: v3 trees are specified in the v3 page) -
Create:
kb/bundles/morphir/morphir-ir/decisions/0018-ir-3-1-adds-specs-and-v3-document-trees.md -
Modify:
kb/bundles/morphir/morphir-ir/decisions/index.md,kb/bundles/morphir/morphir-ir/index.md(bullet),kb/bundles/morphir/morphir-ir/format-version-support.md(lines 13, 30, 75, 77, 78 →[3.0.0,3.2.0); morphir-ui row stays[3.0.0,3.1.0)until it adopts),kb/bundles/morphir/morphir-ir/log.md,docs/design/draft/ir/v3-document-trees.md(status note: implemented, normative pages linked),CHANGELOG.md([Unreleased]) -
Step 1: Write
document-tree-files.mdfor v3 from the draft's "Document tree files" section, in the v4 page's heading order (Serialization profile, Logical paths, File Types 1–4, Validation Rules), stating normative MUSTs: every fileformatVersion"3.1.0"; payloads are classic v3 JSON; the role table (Librarydefunderpkg/,specunderdeps/; Specsspeceverywhere). -
Step 2: Schema: in
morphir-ir-v3.yamlchangedistributiontooneOfof the existing Library 4-tuple and a Specs 4-tuple (const: "Specs",PackageName,Dependencies,PackageSpecification); runmise run website:build-schemasthenmise run check:schemas-generated→ "Every JSON schema … matches its YAML source"; restorewebsite/static/validation-inline.worker.jsif the build rewrote it (git checkout -- website/static/validation-inline.worker.js). -
Step 3: Decision Record 0018 with frontmatter exactly like 0016 (
type: Decision Record,title,description,state: Accepted,decided: <today>,tags: [ir, ir-v3, versioning, format-version, document-tree],status: draft) and sectionsSummary,Why(lowest-version writers; tree reuse; generic layout),Alternatives rejected(always 3.1.0; classic fragments verbatim; v4 spelling at 3; IonElementas the kit's model),Consequences(bindings raise their tables independently; morphir-ui stays 3.0),Revisit when. Add the index bullet (description text identical to frontmatter). Run./target/debug/morphir kb check --no-banner→ no findings;./target/debug/morphir kb refresh markdown --no-bannerif it reports drift. -
Step 4: Changelog
[Unreleased]:### Added— IR 3.1.0 (v3 Specs; v3 JSON/YAML trees;migrate --target-version v3 --output-layout vfs);### Changed— support table[3.0.0,3.2.0),[4.0.0,4.1.0)(breaking for tooling that asserted 3.1.0 is refused). -
Step 5:
mise run docs:cli(no drift expected);cargo test -q -p morphir --no-fail-fast;cargo clippy -q -p morphir --all-targets -- -D warnings;./target/debug/morphir kb check --no-banner. -
Step 6: Commit
docs(ir): specify IR 3.1.0 and v3 document trees; push (MORPHIR_SKIP_HOOKS=1), open the finos/morphir PR (feat(ir): IR 3.1.0 — v3 Specs and v3 JSON/YAML document trees), link Support IR v3 in JSON and YAML document trees #970 and discussion IR 3.1.0: a v3 Specs distribution and v3 JSON/YAML document trees #978; merge on approval; confirmmainCI (acancelledrun means watch the nextmainrun containing the merge).
Task 14: Close out
- Step 1: File follow-up issues (normal English bodies): TypeScript v3 trees and 3.1.0 (morphir-typescript); support-table raises for morphir-elm, morphir-scala, morphir-python (one each, citing decision 0018); and a bead
Move the Ion tree onto the shared layout (TreeParts<Element>)under epicmorphir-vvgi(bd create … --parent morphir-vvgi). - Step 2: Comment on Support IR v3 in JSON and YAML document trees #970 and discussion IR 3.1.0: a v3 Specs distribution and v3 JSON/YAML document trees #978 with the merged PRs;
bd close morphir-f22s --reason "<PRs, main CI green>";bd sync.
|
The design is implemented and has landed. See the summary on #970 for the merged PRs and the follow-ups. The normative pages are |
Uh oh!
There was an error while loading. Please reload this page.
This proposal adds IR 3.1.0: a v3
Specsdistribution, and v3 support in the JSON and YAML document trees. It is the design for #970, and it follows the draft Ion work in #946, whose Ion tree already holds v3. The draft lives atdocs/design/draft/ir/v3-document-trees.mdon branchdocs/ir-3-1-v3-trees. Comments here feed that draft before implementation starts.Decisions already taken in design review:
pathBudget,fileNames, canonical-string names) and carries the classic v3 JSON as itsdeforspecpayload.3. A Specs distribution and every v3 tree are3.1.0.TreeParts<P>), with per-version models. The Ion tree moves onto it later.3.1.0in follow-up issues.This draft adds a minor revision of the classic IR,
3.1.0. The revision adds two things. A v3 distribution may be aSpecsdistribution, and a v3 distribution may be stored as a JSON or YAML document tree. Before this revision, only the Ion tree held v3 (issue 970).A
3.0.0document stays valid and keeps its meaning. A writer emits the lowest version that expresses its content. A reader that implements this draft accepts[3.0.0,3.2.0).Version rules
formatVersiona writer emitsLibrary3(the3.0.0release), unchangedSpecs"3.1.0""3.1.0"in every tree fileA reader accepts the integer
3and the release strings"3.0.0"and"3.1.0".3.2.0and later fail withunsupported_format_version_minor, as format-version support describes. The reference support table becomes[3.0.0,3.2.0),[4.0.0,4.1.0).A binding that stays on
[3.0.0,3.1.0)still reads every3.0.0document. It refuses a Specs distribution and every v3 tree with the minor-version diagnostic, which is the behaviour decision 0016 requires.The Specs distribution
A v3
Specsdistribution publishes a package's specification without its definitions. It mirrorsLibrary:{ "formatVersion": "3.1.0", "distribution": [ "Specs", [["my"], ["pkg"]], [ [ [["morphir"], ["s", "d", "k"]], { "modules": [ … ] } ] ], { "modules": [ [ [["basics"]], { "types": [ … ], "values": [ … ], "doc": null } ] ] } ] }The third element lists dependency specifications, as
Librarydoes. The fourth element is a package specification: its modules are module specifications, spelled as a dependency's modules are.The Ion spelling uses
kind: specsin themorphir::header and writes the distribution's own modules asmodule::specvalues, as the v4 Ion spelling does.Document tree files
A v3 tree uses the v4 tree's logical paths, profiles, escaping, truncation and
fileNamesrules without change (document-tree files). Only the file contents differ. Every file carries"formatVersion": "3.1.0". A file whose version differs from the manifest's is rejected at that file.Envelope members hold canonical strings, the same as the v4 envelope:
package,path,name,dependencies, and the keys offileNames. A canonical string comes from the classic word arrays, so[["morphir"], ["s", "d", "k"]]ismorphir/SDK. Adeforspecpayload is the classic v3 JSON for that entry, exactly as a single-file document writes it.Distribution manifest
{ "formatVersion": "3.1.0", "distribution": "Library", "package": "my/pkg", "pathBudget": 4000, "dependencies": ["morphir/SDK"] }distributionisLibraryorSpecs. A v3 tree has noentryPoints.dependencieslists thedeps/packages in order and is omitted when empty.pathBudgetis required, with the v4 floor of 64.Module manifest
{ "formatVersion": "3.1.0", "path": "domain/user", "doc": "User records.", "types": ["user"], "values": ["find"] }Under
pkg/in aLibrary,accessis written only when it isPrivate. ASpecstree underpkg/, and every tree underdeps/, holds module specifications, which have noaccess.typesandvalueslist names. A reader also accepts inline entries keyed by canonical name, whose values are node payloads, as the v4 reader does.fileNamesrecords cut stems, as in v4.Type and value files
{ "formatVersion": "3.1.0", "name": "user", "def": { "access": "Public", "value": { "doc": "", "value": ["TypeAliasDefinition", [], <Type>] } } } { "formatVersion": "3.1.0", "name": "int", "spec": { "doc": "", "value": ["OpaqueTypeSpecification", []] } } { "formatVersion": "3.1.0", "name": "find", "def": { "access": "Public", "value": { "doc": "", "value": { "inputTypes": [ … ], "outputType": <Type>, "body": <Value> } } } } { "formatVersion": "3.1.0", "name": "add", "spec": { "doc": "", "value": { "inputs": [ … ], "output": <Type> } } }A
Librarytree holdsdeffiles underpkg/andspecfiles underdeps/. ASpecstree holdsspecfiles in both. Adefpayload keeps v3's attribute slots, including a value's inferred types.Implementation in morphir-rust
The kit's layout keeps one implementation of the rules the compatibility kit pins, over a generic payload:
TreeParts<P>holds the manifest envelope and each module's root, package, path, access, doc, and(name, payload)for its types and values. APayloadtrait lets the layout readformatVersion,nameand listings from a file of typeP, and build one.serde_json::Valueimplements it for the JSON and YAML profiles.TreeModeltrait turnsTreePartsinto a distribution and back. The v4 model keeps today's decoders. The v3 model decodes and encodes the classic types.read_treeandwrite_treestay v4.read_tree_v3,write_tree_v3and the per-module streaming writers are new.read_any_treedispatches on the manifest'sformatVersion.version_mismatch.morphir ir migratewrites v3 JSON and YAML trees, and version detection reads any manifest.ir_storage::read_valuereads any v3 tree. Compile output stays v4.The Ion tree keeps its own layout code for now. A follow-up moves it onto
TreeParts<ion_rs::Element>, so that the path, stem, budget and stray-file rules exist once for all three profiles.Compatibility kit
New cases, each marked
version=3:document-tree: a v3 manifest; aLibrarymodule withdeffiles;deps/withspecfiles; aSpecstree; a cut stem recorded infileNames; the YAML profile of one tree; a v4 file inside a v3 tree, rejected.versions:3.1.0accepted,3.2.0rejected.distributions: a single-file v3Specsdistribution.The Rust adapter declares these cases. The TypeScript binding runs them as pending until its follow-up lands.
Delivery
3.1.0in the classic model:Specs, the version strings, the semantic header, the JSON, YAML and Ion single-file codecs, and the support table with its conformance corpus.docs/spec/ir/schemas/v3/document-tree-files.mdand the 3.1.0whats-new), the v3 schema'sSpecs, the compatibility-kit cases, a kb Decision Record, and the changelog.Out of scope
3.1.0, and the3.1.0support-table raises in morphir-elm, morphir-scala and morphir-python, are follow-up issues.write_v3keeps writing single files.All reactions