diff --git a/.config/mise/config.toml b/.config/mise/config.toml index 46ae2223d..18857c0c9 100644 --- a/.config/mise/config.toml +++ b/.config/mise/config.toml @@ -298,16 +298,28 @@ description = "Run and recheck the independent pinned shared MEP guest suite" run = "moonx scripts/portable-mep-mck.mbtx" [tasks."test:portable-mep-cli"] -description = "Run the saved MEP guest through the primary CLI extension command" -run = "moonx scripts/portable-mep-cli.mbtx" +description = "Qualify the portable MoonBit guest (morphir-bytes-v1 WASM) on the worker host from the native and npm CLIs, with the shared MEP kit; [--json]" +run = "moonx scripts/extension-qualification.mbtx moonbit-portable" [tasks."test:wasm-workers"] description = "Run the package tests of one target with the end-to-end WASM worker tests, or the browser Web Worker proof: --target native|js [--scope workers|browser] [--json]" run = "moonx scripts/wasm-workers-test.mbtx" [tasks."test:cli-extism-python"] -description = "Qualify an explicit local Python Extism bundle through native and packaged CLI entry points" -run = "moonx scripts/extism-python.mbtx" +description = "Qualify the pinned Python extension 0.4.1 and 0.4.2 releases on the worker host from the native and npm CLIs; [--python PATH] [--json]" +run = "moonx scripts/extension-qualification.mbtx python" + +[tasks."test:cli-extension-elm"] +description = "Qualify the pinned Elm extension v0.4.1 executables on the worker host from the native and npm CLIs; [--json]" +run = "moonx scripts/extension-qualification.mbtx elm" + +[tasks."test:extension-conformance"] +description = "Run the morphir-extension-sdk conformance fixture guest on the worker host from the native and npm CLIs; [--json]" +run = "moonx scripts/extension-qualification.mbtx conformance" + +[tasks."test:extension-qualification"] +description = "Run every worker host qualification suite (Elm, Python, MoonBit native and portable, SDK fixture); [--python PATH] [--json]" +run = "moonx scripts/extension-qualification.mbtx all" [tasks."sdk-spec:generate"] description = "Regenerate pkgs/morphir-sdk-spec/sdk_data.mbt from pkgs/morphir-sdk-spec/sdk.scm" @@ -510,8 +522,8 @@ description = "Smoke-test CLI targets and installed package" run = "moonx scripts/typescript-tooling.mbtx test-cli" [tasks."test:cli-native-prerequisites"] -description = "Focused native source-CLI compile, provider and local repository acceptance; requires explicit --shared or MORPHIR_SHARED_SOURCE" -run = "moon run scripts/native-services.mbtx -- --acceptance" +description = "Qualify the native MoonBit guest as a process worker on the worker host from the native and npm CLIs, with the shared MEP kit; [--json]" +run = "moonx scripts/extension-qualification.mbtx moonbit-native" [tasks."check:typescript"] description = "Install frozen dependencies and check all TypeScript projects" diff --git a/.gitattributes b/.gitattributes index b23cd54e7..61797b47a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,10 @@ tools/packaged-targets-pin.json text eol=lf # Keep the complete pinned Archify package byte-exact and collapsed in code review. .agents/skills/archify/** -text linguist-vendored .agents/skills/archify/UPSTREAM.md text eol=lf linguist-vendored=false +# Qualification fixtures: their digests and the IR they compile to must not +# depend on the checkout's line endings. +apps/morphir/fixtures/sdk-guest/** text eol=lf +apps/morphir/fixtures/sdk-guest/*.wasm binary +apps/morphir/fixtures/elm-extension/** text eol=lf +apps/morphir/fixtures/extism-python/** text eol=lf +apps/morphir/fixtures/qualification/** text eol=lf diff --git a/.gitignore b/.gitignore index 6e9d5fce6..ebafe7ab5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ target **/*.mbt.js **/*.wat **/*.wasm +# The SDK conformance fixture guest is stored with its source and build receipt. +!apps/morphir/fixtures/sdk-guest/morphir-sdk-fixture-guest.wasm _build/ pkgs/morphir-host/embedding/dist/ diff --git a/apps/morphir/docs/extension.md b/apps/morphir/docs/extension.md index 8610a7702..b69a48248 100644 --- a/apps/morphir/docs/extension.md +++ b/apps/morphir/docs/extension.md @@ -433,10 +433,63 @@ session cases: MEP `0.1` compile from claimed sources, an unclaimed operation, a unsuccessful answer, a failed shutdown, cancellation during generate and the deadline. -`mise run test:portable-mep-cli`, `mise run test:cli-native-prerequisites` and -`mise run test:cli-extism-python` still write retired pins. They are rewritten for -contract `0.2.0` pins and the worker host when the published extensions are -qualified on it (group 7 of the OpenSpec change `host-extensions-as-workers`). +### Published extensions and fixture guests + +The published extensions are examples of conformant extensions, not the +definition of support. `scripts/extension-qualification.mbtx` runs them, and an +independent fixture guest, from the native CLI and from the npm CLI, as a user +installs it (an unpacked `npm pack` of `apps/morphir`). Every task accepts +`--json`: + +| Task | Suite | What it checks | +| --- | --- | --- | +| `mise run test:cli-extension-elm` | `elm` | Elm extension executables `v0.4.1` and elm-classic `v0.5.1` (process workers): describe with the release descriptor, compile of `apps/morphir/fixtures/elm-extension/v1/src`, and no `node` on the PATH of the native run. Elm `v0.5.0-preview.2` (one WASM module, `morphir-bytes-v1`): describe, compile of `v1/package` on the model line, generate, and recompile of the generated sources to the same model | +| `mise run test:cli-extism-python` | `python` | Python extension `0.4.1`: one document for IR 3 and 4, and the refusal of two documents. `0.4.2`: compile of the two-module fixture for IR 3 and 4, generate, a consumer check of the generated Python, recompile of the generated files to the same IR, and the IR version check | +| `mise run test:cli-native-prerequisites` | `moonbit-native` | The native MoonBit guest as a process worker: the shared kit's source, consumer and JSON artifacts through both CLIs, and the shared MEP kit run against the guest | +| `mise run test:portable-mep-cli` | `moonbit-portable` | The portable MoonBit guest (`morphir-bytes-v1`): the same checks, with the shared kit run through `morphir extension worker` of the native CLI | +| `mise run test:extension-conformance` | `conformance` | The `morphir-extension-sdk` fixture guest in `apps/morphir/fixtures/sdk-guest` (see its README): build receipt, WASI and Morphir host function imports, claims of a language that no host knows, compile, generate, recompile, host function answers and guest diagnostics | +| `mise run test:extension-qualification` | `all` | All five suites | + +Each run builds the CLIs and the guests that it needs. It downloads each +pinned artifact of `apps/morphir/fixtures/qualification/published.json` into a +new, empty directory under `.dev/extension-qualification/downloads` and uses it +only when its SHA-256 equals the pin. It writes one receipt for each suite, +`.dev/extension-qualification/evidence/.json` (`--evidence DIR` selects +another directory). A receipt names the platform, the CLI build (commit, a dirty +flag and the program SHA-256) and one row for each operation: extension, +version, CLI (`native`, `npm` or `mck` for the shared kit runner), operation, +result, guest SHA-256, descriptor SHA-256 and the digests of the published +files. With `--json`, stdout is one `morphir/command-result` with `result.suites` +(state and row counts of each receipt) and `result.receipts`; build and driver +output goes to stderr. A failed suite exits 1 with `qualification_failed`. + +A row is `passed`, `failed` or `known-failure`. A known failure is an operation +that fails for a recorded cause outside the host, with its finding and +follow-up bead. It does not fail the suite, and it fails the suite when it +passes, so the record is removed. The current known failures are: + +- Elm extension `v0.4.1` compile. The extension requires + `package.exposedModules` in `morphir.frontend.compile`; the MEP model makes it + optional and the host, which conforms, does not send it. Describe passes. + Bead `morphir-ukvn.5.64.36.16`: elm-classic `v0.5.1` (the same JavaScript + extension, renamed) fixes it, and its compile passes. +- The shared MEP kit against the MoonBit guest. The kit + `spec/mep/mck/guest-moonbit-bool.json` asks for the frontend profile + `moonbit-model-bool-library-v1`, which the guest retired, so the case + `compile-ion` fails after describe, initialize and the notifications pass. + Follow-up: bead `morphir-ukvn.5.64.36.17` (finos/morphir). For the same + reason `mise run test:portable-mep-mck` does not pass. + +No shared kit profile fits the SDK fixture guest, because each shared profile +names its own extension. The `conformance` suite is therefore a local check. A +generic kit profile is bead `morphir-ukvn.5.64.36.15`. + +These tasks are manual. No pull request or merge-queue job runs them, because +they download published releases and build several guests. The workflow +`.github/workflows/extension-qualification.yml`, started only by +`workflow_dispatch`, runs one suite on one runner through +`scripts/extension-qualification-run.mbtx`, which saves the JSON result for the +artifact and keeps the qualification's exit status. ## Controlled local repositories @@ -484,24 +537,8 @@ Every lifecycle command accepts `--json` and writes one `morphir/command-result` after required writes. These commands do not automatically join installed selection to invocation. That downstream acceptance remains morphir-ukvn.5.64.30. -**Pending.** `mise run test:cli-native-prerequisites` still writes retired pins and -calls the removed helper route, so it does not pass against this CLI. It is -rewritten for contract `0.2.0` pins and the worker host in group 7 of the OpenSpec -change `host-extensions-as-workers`; see [Qualification](#qualification). Until -then, the description below records what the task covered before the worker host: - -```sh -mise run test:cli-native-prerequisites -- --shared /path/to/finos/morphir --json -``` - -It freezes separately built native guests and fault peers, tests embedded Boolean -and Elm compilation, compile/retain/generate/consumer execution, malformed/oversized -frames, tuple and context refusal, crashes, diagnostics, deadlines, cancellation, -shutdown and cleanup. It also tests local publication/install, a real version -update, corrupt and disabled selections, offline replay and removal in a fresh -home. It reruns the existing portable provider tests and CLI unit suite. The task -passes the produced CLI, adapter and guest paths explicitly to each test process, -binding its receipts to tested artifacts even when the caller has artifact -override variables set. Evidence and host pins stay under `.dev/native-services`; -no installed matrix, new CI job, -release or default switch is performed. +`mise run test:cli-native-prerequisites` used to test these commands with the +retired helper route. It now qualifies the native MoonBit guest on the worker +host; see [Qualification](#qualification). The Rust adapter acceptance stays +available as `moon run scripts/native-services.mbtx -- --acceptance --shared +/path/to/finos/morphir --json` until the helper routes are removed. diff --git a/apps/morphir/fixtures/elm-extension/v1/README.md b/apps/morphir/fixtures/elm-extension/v1/README.md new file mode 100644 index 000000000..32927c8a7 --- /dev/null +++ b/apps/morphir/fixtures/elm-extension/v1/README.md @@ -0,0 +1,14 @@ +# Elm extension qualification fixtures + +`mise run test:cli-extension-elm` compiles these with the published Elm +extensions. + +- `src/Rules.elm`, package `acme/rules`, for the executables of `v0.4.1` and + elm-classic `v0.5.1`. They do not claim `multiDocument`, so the fixture is one + document. Its module name has one segment, because these extensions compile + only module names that the package name does not prefix. +- `package/Acme/Rules/Main.elm`, package `Acme.Rules`, for the `v0.5.0-preview.2` + WASM module. That extension requires each module name to start with the + package name and to have one more segment, and the file path to match it. + +Both hold the same two functions. diff --git a/apps/morphir/fixtures/elm-extension/v1/package/Acme/Rules/Main.elm b/apps/morphir/fixtures/elm-extension/v1/package/Acme/Rules/Main.elm new file mode 100644 index 000000000..4d8289065 --- /dev/null +++ b/apps/morphir/fixtures/elm-extension/v1/package/Acme/Rules/Main.elm @@ -0,0 +1,15 @@ +module Acme.Rules.Main exposing (eligible, priority) + + +eligible : Bool -> Bool -> Bool +eligible active vip = + active && not vip + + +priority : Bool -> Int +priority vip = + if vip then + 1 + + else + 2 diff --git a/apps/morphir/fixtures/elm-extension/v1/src/Rules.elm b/apps/morphir/fixtures/elm-extension/v1/src/Rules.elm new file mode 100644 index 000000000..ead69e1a7 --- /dev/null +++ b/apps/morphir/fixtures/elm-extension/v1/src/Rules.elm @@ -0,0 +1,15 @@ +module Rules exposing (eligible, priority) + + +eligible : Bool -> Bool -> Bool +eligible active vip = + active && not vip + + +priority : Bool -> Int +priority vip = + if vip then + 1 + + else + 2 diff --git a/apps/morphir/fixtures/extism-python/v1/README.md b/apps/morphir/fixtures/extism-python/v1/README.md index 6e165362c..ce6dbc151 100644 --- a/apps/morphir/fixtures/extism-python/v1/README.md +++ b/apps/morphir/fixtures/extism-python/v1/README.md @@ -10,7 +10,12 @@ The local `large_value` function is an additional precision regression. Its integer exceeds JavaScript's exact-number range and must survive generation without rounding. -The focused CLI suite compares native Python API compilation with the real -published WASM bundle for IR 3 and 4, generates Python, checks selected consumer -behavior, and recompiles without changing the IR. This proves only this bounded -subset and the exact host/CLI cells in the resulting receipt. +`single/rules.py` is one module with no imports, for the Python extension +`0.4.1`, which does not claim `multiDocument` and compiles one document for each +request. The two-module `acme` directory is its multi-document refusal. + +`mise run test:cli-extism-python` compiles `acme` with the published `0.4.2` +module for IR 3 and 4 from the native and npm CLIs, generates Python, runs +`apps/morphir/scripts/extension-qualification-consumer.py` on the generated +files, and recompiles them without a change to the IR. This proves only this +bounded subset and the exact host and CLI cells in the resulting receipt. diff --git a/apps/morphir/fixtures/extism-python/v1/single/rules.py b/apps/morphir/fixtures/extism-python/v1/single/rules.py new file mode 100644 index 000000000..e82de9cce --- /dev/null +++ b/apps/morphir/fixtures/extism-python/v1/single/rules.py @@ -0,0 +1,12 @@ +from __future__ import annotations + + +def choose(flag: bool, first: int, second: int) -> int: + if flag: + return first + else: + return second + + +def large_value() -> int: + return 9007199254740993 diff --git a/apps/morphir/fixtures/qualification/published.json b/apps/morphir/fixtures/qualification/published.json new file mode 100644 index 000000000..6bc1b504b --- /dev/null +++ b/apps/morphir/fixtures/qualification/published.json @@ -0,0 +1,159 @@ +{ + "contractVersion": "0.1.0", + "elm": { + "repository": "finos/morphir-elm", + "tag": "extension/elm/v0.4.1", + "version": "0.4.1", + "sourceRevision": "a3299f95c9252852f19c1ccabbb0d1525a24a25e", + "descriptor": { + "filename": "morphir-elm-extension-0.4.1.bundle.release.json", + "sha256": "152ee03ddd4e9e0b91fbe0d6a79f6228387ce35c3f42a32f844ababf0c1c924d" + }, + "executables": { + "aarch64-apple-darwin": { + "filename": "morphir-elm-extension-0.4.1-aarch64-apple-darwin", + "sha256": "7b4ea687589c67e561162cc1cf7cac78d93d49cfc2dfc4e6fd17217c6b7139e9" + }, + "x86_64-apple-darwin": { + "filename": "morphir-elm-extension-0.4.1-x86_64-apple-darwin", + "sha256": "5ed08c452bc70ab8c70b3cdcfb50271e3e828a840cb77b214a799a4718c98196" + }, + "aarch64-unknown-linux-gnu": { + "filename": "morphir-elm-extension-0.4.1-aarch64-unknown-linux-gnu", + "sha256": "b9394d624b6962be93272b587c35379fb7ccab2468ed525e819233e8047ebd72" + }, + "x86_64-unknown-linux-gnu": { + "filename": "morphir-elm-extension-0.4.1-x86_64-unknown-linux-gnu", + "sha256": "2f172a7f3979962ac1cb0203457a0b8178324712c8b58e21c578c68d626d46bb" + }, + "aarch64-pc-windows-msvc": { + "filename": "morphir-elm-extension-0.4.1-aarch64-pc-windows-msvc.exe", + "sha256": "1fa57c943b209c2065499f2dcd93d2f5039d1fe82a22b2292a7d7253e660a97b" + }, + "x86_64-pc-windows-msvc": { + "filename": "morphir-elm-extension-0.4.1-x86_64-pc-windows-msvc.exe", + "sha256": "5049f4ab97671ef54d183157c82b13a6687b6de5da33566756ae4c3e627d49d8" + } + } + }, + "elmClassic": { + "repository": "finos/morphir-elm", + "tag": "extension/elm-classic/v0.5.1", + "version": "0.5.1", + "sourceRevision": "947a65eea45d38c4876c91a922753a24443ebf70", + "descriptor": { + "filename": "morphir-elm-classic-extension-0.5.1.bundle.release.json", + "sha256": "ccf251ff77d6a80cd5412fbc2a56b281b50fc6a4f2ebad3a9379168bee730ffe" + }, + "executables": { + "aarch64-apple-darwin": { + "filename": "morphir-elm-classic-extension-0.5.1-aarch64-apple-darwin", + "sha256": "43716cdff0975ad43ad810ca37b3a7d43a0130c7d4509b72814ef656faf2cc04" + }, + "x86_64-apple-darwin": { + "filename": "morphir-elm-classic-extension-0.5.1-x86_64-apple-darwin", + "sha256": "e6e6df4ffa4dec8cbbe666b106def83e3dff31c45210595175629c58aa9a701f" + }, + "aarch64-unknown-linux-gnu": { + "filename": "morphir-elm-classic-extension-0.5.1-aarch64-unknown-linux-gnu", + "sha256": "151d22959d4463e2f4263ec3892c92fe9994a7895e228062192511de5b7b7b85" + }, + "x86_64-unknown-linux-gnu": { + "filename": "morphir-elm-classic-extension-0.5.1-x86_64-unknown-linux-gnu", + "sha256": "9cfcaf23353e83723cd0b153ceb863a9c0470e79e344e0ab890a36fa3eba3b37" + }, + "aarch64-pc-windows-msvc": { + "filename": "morphir-elm-classic-extension-0.5.1-aarch64-pc-windows-msvc.exe", + "sha256": "e962f2d4877dddef9d063efbcd872db14a63a5166120a7f095cd25c4bc735836" + }, + "x86_64-pc-windows-msvc": { + "filename": "morphir-elm-classic-extension-0.5.1-x86_64-pc-windows-msvc.exe", + "sha256": "b21d4b6c6f4667b5bbb532cbe7428bef9f8524baaa55db0fdbfb47d0233e3de1" + } + } + }, + "elmWasm": { + "repository": "finos/morphir-elm", + "tag": "extension/elm/v0.5.0-preview.2", + "version": "0.5.0-preview.2", + "sourceRevision": "1604c1108886ef630b877888d4f9ea877bfa32f8", + "module": { + "filename": "morphir-elm-0.5.0-preview.2.wasm", + "sha256": "4d3ea176fdc4c7b19d887c91834ba4cdfb785249e72ce23132a8d04734a8ca6c" + }, + "descriptor": { + "filename": "morphir-elm-0.5.0-preview.2.release.json", + "sha256": "4bc11967398e578175a1d38c6005b3c8e949a7c2c3a5320511a60ad37b5d39eb" + } + }, + "python": { + "repository": "finos/morphir-python", + "releases": { + "0.4.1": { + "tag": "extension/python/v0.4.1", + "sourceRevision": "0d7e2eb62b4b1249129ab702a5240c41266813f6", + "module": { + "filename": "morphir-python-binding-0.4.1.wasm", + "sha256": "a04722e16115667fc7c332177805dcb049aef563afcee0e5f80c5fea302f5b4e" + }, + "descriptor": { + "filename": "morphir-python-binding-0.4.1.release.json", + "sha256": "6cebee18b70e75da90f1cd187f24c8185ddde2565bb7c8e5049e21bbcccf0bb3" + } + }, + "0.4.2": { + "tag": "extension/python/v0.4.2", + "sourceRevision": "1f547d17f648d5b398862f9dfceb05b6d463cb3f", + "module": { + "filename": "morphir-python-binding-0.4.2.wasm", + "sha256": "10acafa0f0282c5240e9620a5f2bb21629cc7580b9add528e7b77c7e54db22b5" + }, + "descriptor": { + "filename": "morphir-python-binding-0.4.2.release.json", + "sha256": "72770f73f05a72535fd8c86ad4629175019037441a758b827c1ba47cc9a52ebd" + } + } + } + }, + "mck": { + "repository": "finos/morphir", + "tag": "v0.4.0-beta.11", + "sourceRevision": "9d4f4b4de0f9bd10549493bdd9c83f20709055f9", + "runners": { + "aarch64-apple-darwin": { + "filename": "morphir-0.4.0-beta.11-aarch64-apple-darwin.tgz", + "sha256": "1a1895c0b189b79c0cbb590c2b43d05c5ea9688bbc4b5cf8be6262cf57c19380" + }, + "x86_64-apple-darwin": { + "filename": "morphir-0.4.0-beta.11-x86_64-apple-darwin.tgz", + "sha256": "e93f826039bbaf95226c7e3cc7698920ae3968ab5c3263633fe0188d334dfcf1" + }, + "aarch64-unknown-linux-gnu": { + "filename": "morphir-0.4.0-beta.11-aarch64-unknown-linux-gnu.tgz", + "sha256": "141c7d75a5539e24f344a55c78b00a7cf22397b155f60f7c00787402660fc28f" + }, + "x86_64-unknown-linux-gnu": { + "filename": "morphir-0.4.0-beta.11-x86_64-unknown-linux-gnu.tgz", + "sha256": "01d0f96bbe73dadf06188d725b823d407c0bf79ef78869d57669744eed9fd437" + }, + "aarch64-pc-windows-msvc": { + "filename": "morphir-0.4.0-beta.11-aarch64-pc-windows-msvc.zip", + "sha256": "4ca0af7b9e2bc9f632ae49baca6baf615eb0171008d1a6e924a981e3a4a347c6" + }, + "x86_64-pc-windows-msvc": { + "filename": "morphir-0.4.0-beta.11-x86_64-pc-windows-msvc.zip", + "sha256": "da1a24c53802fcbb2750813b2fd865334b47b3410bdae9afdc64e5a59abb1cba" + } + }, + "kits": { + "guest-moonbit-bool": { + "path": "spec/mep/mck/guest-moonbit-bool.json", + "sha256": "59485eb156d50d3f4255c67561fb509e3a938aff1739d22a27099043b236a5d0" + }, + "guest-python-json": { + "path": "spec/mep/mck/guest-python-json.json", + "sha256": "9b4b5079ec36275b6ab5f11e7029e40959bc6577dcae41c4659d4151fab45a9d" + } + } + } +} diff --git a/apps/morphir/fixtures/sdk-guest/Cargo.lock b/apps/morphir/fixtures/sdk-guest/Cargo.lock new file mode 100644 index 000000000..107088c81 --- /dev/null +++ b/apps/morphir/fixtures/sdk-guest/Cargo.lock @@ -0,0 +1,1523 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a5b0a5ab8561c603f16c3b78c24a386b1f70e7fc1b1b1e8c15738828e4a176" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "compact_str" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "delegate" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e018fccbeeb50ff26562ece792ed06659b9c2dae79ece77c4456bb10d9bf79b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "encoding_rs" +version = "0.8.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89253175e704ab869b04f9db224d38630e0211a53841a79181deae0889e169f5" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "extism-convert" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f765cafef8be05cf4518613ae2c6b1113e2a1fa630b632e7e8e53f6af8e6bae9" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytemuck", + "extism-convert-macros", + "prost", + "rmp-serde", + "serde", + "serde_json", +] + +[[package]] +name = "extism-convert-macros" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd35d75555de7859923a276a03540ce90778c57e832f6d33359a6b465f4475a" +dependencies = [ + "manyhow", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "extism-manifest" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d8c1cfe930f60da0875e828261f63093f2379e1c2901b3302b5b3b6893a46a" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352fcb5a66eb74145a1c4a01f2bd15d59c62c85be73aac8471880c65b26b798f" +dependencies = [ + "anyhow", + "base64 0.22.1", + "extism-convert", + "extism-manifest", + "extism-pdk-derive", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d086daea5fd844e3c5ac69ddfe36df4a9a43e7218cf7d1f888182b089b09806c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "globset" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98b0d85522dc0a824cfebbc9594ff4a9b289c568bea0370319dfbdf20ccdd2d" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ice_code" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6524844f553e8da5999f3000cf11d3f1ff926bb03fc087441c7b86dee4a7d48" + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ion-rs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "285b4429808446f30da668d210beaeb7ab82defb6045e261c6cdfaeafe565552" +dependencies = [ + "arrayvec", + "base64 0.12.3", + "bumpalo", + "chrono", + "compact_str", + "delegate", + "generic-array", + "ice_code", + "memchr", + "num-bigint", + "num-integer", + "num-traits", + "phf", + "rustc-hash", + "smallvec", + "thiserror 1.0.69", + "visibility", + "winnow 0.6.24", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "morphir-config" +version = "0.0.1" +source = "git+https://github.com/finos/morphir-rust?rev=de35f596a07deb8a3ba25b0f2988b217d111fecf#de35f596a07deb8a3ba25b0f2988b217d111fecf" +dependencies = [ + "secrecy", + "serde", + "serde-saphyr", + "serde_json", + "thiserror 2.0.21", + "toml", +] + +[[package]] +name = "morphir-extension-sdk" +version = "0.4.0" +source = "git+https://github.com/finos/morphir-rust?rev=de35f596a07deb8a3ba25b0f2988b217d111fecf#de35f596a07deb8a3ba25b0f2988b217d111fecf" +dependencies = [ + "base64 0.22.1", + "extism-pdk", + "ion-rs", + "morphir-json", + "morphir-workspace", + "percent-encoding", + "semver", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.21", + "url", +] + +[[package]] +name = "morphir-json" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust?rev=de35f596a07deb8a3ba25b0f2988b217d111fecf#de35f596a07deb8a3ba25b0f2988b217d111fecf" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "morphir-sdk-fixture-guest" +version = "0.1.0" +dependencies = [ + "morphir-extension-sdk", + "serde_json", +] + +[[package]] +name = "morphir-workspace" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust?rev=de35f596a07deb8a3ba25b0f2988b217d111fecf#de35f596a07deb8a3ba25b0f2988b217d111fecf" +dependencies = [ + "globset", + "morphir-config", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "base64 0.23.1", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf_types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" +dependencies = [ + "cfg-if", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/apps/morphir/fixtures/sdk-guest/Cargo.toml b/apps/morphir/fixtures/sdk-guest/Cargo.toml new file mode 100644 index 000000000..0904a0511 --- /dev/null +++ b/apps/morphir/fixtures/sdk-guest/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "morphir-sdk-fixture-guest" +version = "0.1.0" +edition = "2024" +rust-version = "1.98" +license = "Apache-2.0" +publish = false +description = "Conformance fixture guest for the Morphir CLI worker host, built with morphir-extension-sdk" + +# A standalone crate: no parent workspace. +[workspace] + +[lib] +crate-type = ["cdylib"] +path = "src/lib.rs" + +[dependencies] +# Pinned revision of finos/morphir-rust; see build-receipt.json. +morphir-extension-sdk = { git = "https://github.com/finos/morphir-rust", rev = "de35f596a07deb8a3ba25b0f2988b217d111fecf" } +serde_json = "1.0" + +[profile.release] +opt-level = "s" +lto = true +codegen-units = 1 +strip = true +panic = "abort" diff --git a/apps/morphir/fixtures/sdk-guest/README.md b/apps/morphir/fixtures/sdk-guest/README.md new file mode 100644 index 000000000..4e5dfa29e --- /dev/null +++ b/apps/morphir/fixtures/sdk-guest/README.md @@ -0,0 +1,46 @@ +# SDK conformance fixture guest + +This WASM guest shows that the CLI's worker host runs an extension from another +toolchain with no change made for it. It is built in Rust with +`morphir-extension-sdk` from finos/morphir-rust at revision +`de35f596a07deb8a3ba25b0f2988b217d111fecf`, for `wasm32-wasip1`. It is a test +fixture, not a published extension. + +The guest claims a language that no host knows, `morphir-fixture-flags`, with the +file extension `.flags`. A `.flags` document holds one Boolean constant per line: + +```text +# comment +ready = true +blockedNow = false +``` + +Compile writes one Morphir IR 3 module for each document. Generate (target +`morphir-fixture-flags`) writes the constants back as `.flags` documents, so the +generated files compile back to the same IR. + +The module imports the Extism kernel (`extism:host/env`), all four Morphir host +functions (`extism:host/user`: `morphir_get_workspace_info`, `morphir_cache_ir`, +`morphir_get_cached_ir`, `morphir_log`) and WASI preview 1 functions through the +Rust standard library. On each compile the guest uses them and reports the +answers in one `fixture.host` info diagnostic, for example: + +```json +{"cacheRoundTrip":true,"morphirLog":true,"wasiArguments":0,"wasiClock":true,"wasiEnvironment":0,"wasiRandomSeed":true,"workspaceRoot":true} +``` + +`build-receipt.json` records the toolchain, the SDK revision, the source digests, +the full import list and the SHA-256 of `morphir-sdk-fixture-guest.wasm`. To build +it again and compare: + +```sh +cd apps/morphir/fixtures/sdk-guest +cargo +1.98.1 build --release --locked --target wasm32-wasip1 +shasum -a 256 target/wasm32-wasip1/release/morphir_sdk_fixture_guest.wasm +``` + +`mise run test:extension-conformance` runs the guest through the native and npm +CLIs and checks its imports, claims, compile, generate and host function answers. +No shared MEP kit profile fits this guest: the shared kits qualify the MoonBit +Boolean library and the Python JSON library profiles, and both name their own +extension. A generic kit profile is follow-up bead `morphir-ukvn.5.64.36.15`. diff --git a/apps/morphir/fixtures/sdk-guest/build-receipt.json b/apps/morphir/fixtures/sdk-guest/build-receipt.json new file mode 100644 index 000000000..7f40a9e28 --- /dev/null +++ b/apps/morphir/fixtures/sdk-guest/build-receipt.json @@ -0,0 +1,56 @@ +{ + "profile": "morphir/fixture-build-receipt", + "contractVersion": "0.1.0", + "artifact": { + "path": "morphir-sdk-fixture-guest.wasm", + "sha256": "f5e1c9cd7a3472b6261c2c3ebfe2c39aeae14da5287dac6cd6ca87ce854901f1", + "bytes": 752806 + }, + "sdk": { + "crate": "morphir-extension-sdk", + "repository": "https://github.com/finos/morphir-rust", + "revision": "de35f596a07deb8a3ba25b0f2988b217d111fecf" + }, + "toolchain": { + "rustc": "1.98.1 (48a229cea 2026-09-01)", + "cargo": "1.98.1 (797e8a9bc 2026-08-05)", + "target": "wasm32-wasip1", + "host": "aarch64-apple-darwin" + }, + "command": "cargo +1.98.1 build --release --locked --target wasm32-wasip1", + "output": "target/wasm32-wasip1/release/morphir_sdk_fixture_guest.wasm", + "reproduced": "two clean builds in different target directories gave the same SHA-256", + "sources": { + "Cargo.toml": "9764baf299af8c097773b41fa97a1fc0d33faf2b12de100a973a288936309542", + "Cargo.lock": "2b6dac86519d502495dae34b83e642926e9788222a4e09d954097a8643ba6a64", + "src/lib.rs": "252691958e74cdf139ea45b9d20e3c797fa6110efe0e3091795f5f5521ef450d" + }, + "imports": [ + "extism:host/user.morphir_get_workspace_info", + "extism:host/env.length", + "extism:host/env.free", + "extism:host/user.morphir_cache_ir", + "extism:host/user.morphir_get_cached_ir", + "extism:host/env.get_log_level", + "extism:host/env.log_info", + "extism:host/user.morphir_log", + "wasi_snapshot_preview1.args_sizes_get", + "wasi_snapshot_preview1.args_get", + "extism:host/env.input_length", + "extism:host/env.input_load_u64", + "extism:host/env.input_load_u8", + "extism:host/env.error_set", + "extism:host/env.output_set", + "extism:host/env.alloc", + "extism:host/env.store_u64", + "extism:host/env.store_u8", + "extism:host/env.load_u64", + "extism:host/env.load_u8", + "wasi_snapshot_preview1.random_get", + "wasi_snapshot_preview1.environ_get", + "wasi_snapshot_preview1.environ_sizes_get", + "wasi_snapshot_preview1.clock_time_get", + "wasi_snapshot_preview1.fd_write", + "wasi_snapshot_preview1.proc_exit" + ] +} diff --git a/apps/morphir/fixtures/sdk-guest/morphir-sdk-fixture-guest.wasm b/apps/morphir/fixtures/sdk-guest/morphir-sdk-fixture-guest.wasm new file mode 100755 index 000000000..5970f1295 Binary files /dev/null and b/apps/morphir/fixtures/sdk-guest/morphir-sdk-fixture-guest.wasm differ diff --git a/apps/morphir/fixtures/sdk-guest/src/lib.rs b/apps/morphir/fixtures/sdk-guest/src/lib.rs new file mode 100644 index 000000000..a4ba1d47c --- /dev/null +++ b/apps/morphir/fixtures/sdk-guest/src/lib.rs @@ -0,0 +1,358 @@ +//! A conformance fixture guest for the Morphir CLI worker host. +//! +//! It is built with `morphir-extension-sdk` for `wasm32-wasip1`, so it imports +//! WASI preview 1 through the Rust standard library and the Morphir host +//! functions through the SDK. It is not a published extension. Its frontend +//! claims a language that no host knows, `morphir-fixture-flags` with files +//! `.flags`, so that a host that runs it shows that it holds no list of +//! languages or extensions. +//! +//! A `.flags` document holds Boolean constants, one per line: +//! +//! ```text +//! # comment +//! ready = true +//! blocked = false +//! ``` +//! +//! Compile gives one Morphir IR 3 module per document, with one Boolean +//! constant per line. Generate turns that IR back into `.flags` documents, so +//! generated output compiles back to the same IR. +//! +//! Each compile also uses the host: it reads the workspace information, writes +//! and reads the session IR cache, logs through the Extism kernel and through +//! `morphir_log`, and calls WASI (clock, random seed, arguments, environment, +//! stderr). It reports what it saw as one `fixture.host` info diagnostic. + +use morphir_extension_sdk::prelude::*; +use morphir_extension_sdk::{ + Artifact, BackendCapability, CompileRequest, CompileResult, Diagnostic, DiagnosticSeverity, + FrontendCapability, GenerateRequest, GenerateResult, LanguageCapability, export_extension, +}; +use serde_json::{Value, json}; + +/// The language that this fixture compiles. No host has code for it. +const LANGUAGE: &str = "morphir-fixture-flags"; +const FILE_EXTENSION: &str = ".flags"; +const TARGET: &str = "morphir-fixture-flags"; + +#[derive(Default)] +pub struct FixtureGuest; + +impl Extension for FixtureGuest { + fn info() -> ExtensionInfo { + ExtensionInfo { + id: "morphir-sdk-fixture".into(), + name: "Morphir SDK conformance fixture".into(), + version: "0.1.0".into(), + description: Some( + "Worker host conformance fixture built with morphir-extension-sdk".into(), + ), + types: vec![], + author: None, + homepage: None, + license: Some("Apache-2.0".into()), + min_sdk_version: None, + } + } + + fn capabilities() -> ExtensionCapabilities { + ExtensionCapabilities { + frontend: Some(FrontendCapability { + languages: vec![LanguageCapability { + id: LANGUAGE.into(), + file_extensions: vec![FILE_EXTENSION.into()], + }], + ir_versions: vec!["3".into()], + compile: true, + incremental: false, + fragments: false, + multi_document: true, + }), + backend: Some(BackendCapability { + targets: vec![TARGET.into()], + ir_versions: vec!["3".into()], + generate: true, + }), + ..Default::default() + } + } +} + +impl Frontend for FixtureGuest { + fn compile(&self, request: CompileRequest) -> morphir_extension_sdk::Result { + let mut diagnostics = vec![host_report()]; + if request.language_id != LANGUAGE { + diagnostics.push(error( + "fixture.language", + format!("this fixture compiles {LANGUAGE}, not {}", request.language_id), + )); + return Ok(failed(diagnostics)); + } + if request.options.ir_version != "3" && request.options.ir_version != "3.0.0" { + diagnostics.push(error( + "fixture.ir_version", + format!("this fixture writes IR 3, not {}", request.options.ir_version), + )); + return Ok(failed(diagnostics)); + } + let mut documents: Vec<_> = request.sources.documents.iter().collect(); + documents.sort_by(|left, right| left.uri.cmp(&right.uri)); + let mut modules = Vec::new(); + for document in documents { + match module(&document.uri, &document.text) { + Ok(module) => modules.push(module), + Err(message) => diagnostics.push(error("fixture.syntax", message)), + } + } + if diagnostics.len() > 1 { + return Ok(failed(diagnostics)); + } + let ir = json!({ + "formatVersion": 3, + "distribution": ["Library", path(&request.package.name), [], {"modules": modules}], + }); + Ok(CompileResult { + success: true, + ir_version: Some("3".into()), + ir: Some(ir), + diagnostics, + modules: vec![], + module_results: vec![], + context_digest: None, + }) + } + + fn supported_languages() -> Vec { + vec![LANGUAGE.into()] + } + + fn file_extensions() -> Vec { + vec![FILE_EXTENSION.into()] + } +} + +impl Backend for FixtureGuest { + fn generate(&self, request: GenerateRequest) -> morphir_extension_sdk::Result { + match artifacts(&request.ir) { + Ok(artifacts) => Ok(GenerateResult { + success: true, + artifacts, + diagnostics: vec![], + }), + Err(message) => Ok(GenerateResult { + success: false, + artifacts: vec![], + diagnostics: vec![error("fixture.unsupported_ir", message)], + }), + } + } + + fn target_languages() -> Vec { + vec![TARGET.into()] + } +} + +export_extension!(FixtureGuest, frontend, backend); + +/// The Boolean type reference of the Morphir SDK, in IR 3 JSON. +fn bool_type() -> Value { + json!(["Reference", {}, [[["morphir"], ["s", "d", "k"]], [["basics"]], ["bool"]], []]) +} + +/// A Morphir name: lower-case words, split at `_`, `-`, `.` and capitals. +fn name(text: &str) -> Value { + let mut words: Vec = Vec::new(); + let mut word = String::new(); + for character in text.chars() { + if character == '_' || character == '-' || character == '.' { + if !word.is_empty() { + words.push(std::mem::take(&mut word)); + } + } else if character.is_uppercase() { + if !word.is_empty() { + words.push(std::mem::take(&mut word)); + } + word.extend(character.to_lowercase()); + } else { + word.push(character); + } + } + if !word.is_empty() { + words.push(word); + } + json!(words) +} + +/// A Morphir path from a `/`-separated package name. +fn path(text: &str) -> Value { + Value::Array(text.split('/').filter(|part| !part.is_empty()).map(name).collect()) +} + +/// The module name of a document: its file name without `.flags`. +fn module_name(uri: &str) -> std::result::Result { + let file = uri.rsplit('/').next().unwrap_or(uri); + match file.strip_suffix(FILE_EXTENSION) { + Some(stem) if !stem.is_empty() => Ok(stem.to_string()), + _ => Err(format!("{uri}: a document name must end with {FILE_EXTENSION}")), + } +} + +fn module(uri: &str, text: &str) -> std::result::Result { + let module = module_name(uri)?; + let mut values = Vec::new(); + for (index, line) in text.lines().enumerate() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let parse = || -> Option<(&str, bool)> { + let (key, value) = line.split_once('=')?; + let key = key.trim(); + let valid = !key.is_empty() + && key.chars().next()?.is_ascii_lowercase() + && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); + let value = match value.trim() { + "true" => true, + "false" => false, + _ => return None, + }; + valid.then_some((key, value)) + }; + let Some((key, value)) = parse() else { + return Err(format!( + "{uri}:{}: expected `name = true` or `name = false`", + index + 1 + )); + }; + values.push(json!([name(key), {"access": "Public", "value": {"doc": "", "value": { + "inputTypes": [], + "outputType": bool_type(), + "body": ["Literal", bool_type(), ["BoolLiteral", value]], + }}}])); + } + Ok(json!([[name(&module)], {"access": "Public", "value": { + "types": [], "values": values, "doc": null, + }}])) +} + +/// Joins the words of a Morphir name with `_`, the form that compiles back +/// to the same name. +fn words(value: &Value) -> Option { + let words: Option> = value.as_array()?.iter().map(Value::as_str).collect(); + Some(words?.join("_")) +} + +fn artifacts(ir: &Value) -> std::result::Result, String> { + let unsupported = || "expected a Morphir IR 3 Library of Boolean constants".to_string(); + if ir["formatVersion"] != json!(3) || ir["distribution"][0] != json!("Library") { + return Err(unsupported()); + } + let modules = ir["distribution"][3]["modules"].as_array().ok_or_else(unsupported)?; + let mut artifacts = Vec::new(); + for module in modules { + let module_path = module[0].as_array().ok_or_else(unsupported)?; + let file: Option> = module_path.iter().map(words).collect(); + let file = file.ok_or_else(unsupported)?.join("/"); + let mut content = String::new(); + for value in module[1]["value"]["values"].as_array().ok_or_else(unsupported)? { + let key = words(&value[0]).ok_or_else(unsupported)?; + let body = &value[1]["value"]["value"]["body"]; + let literal = body[2][1].as_bool().filter(|_| body[0] == json!("Literal")); + let literal = literal.ok_or_else(unsupported)?; + content.push_str(&format!("{key} = {literal}\n")); + } + artifacts.push(Artifact { + path: format!("{file}{FILE_EXTENSION}"), + content, + binary: false, + }); + } + Ok(artifacts) +} + +fn error(code: &str, message: String) -> Diagnostic { + Diagnostic { + severity: DiagnosticSeverity::Error, + code: Some(code.into()), + message, + location: None, + related: vec![], + } +} + +fn failed(diagnostics: Vec) -> CompileResult { + CompileResult { + success: false, + ir_version: None, + ir: None, + diagnostics, + modules: vec![], + module_results: vec![], + context_digest: None, + } +} + +/// Uses the host functions and WASI, and reports what the host answered. +fn host_report() -> Diagnostic { + let report = observe_host(); + Diagnostic { + severity: DiagnosticSeverity::Info, + code: Some("fixture.host".into()), + message: report.to_string(), + location: None, + related: vec![], + } +} + +#[cfg(target_arch = "wasm32")] +fn observe_host() -> Value { + use morphir_extension_sdk::host; + let workspace = host::get_workspace_info(); + let key = "fixture/cache-probe"; + let probe = json!({"probe": [1, 2, 3]}); + host::cache_ir(key, &probe); + let cached = host::get_cached_ir(key); + host::log("info", "fixture guest: Extism kernel log"); + let logged = log::send("info", "fixture guest: morphir_log"); + // WASI preview 1: clock_time_get, random_get (the map's seed), + // args_sizes_get, environ_sizes_get and fd_write on stderr. + let clock = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .is_ok(); + let mut seeded = std::collections::HashMap::new(); + seeded.insert("seed", 1); + let arguments = std::env::args().count(); + let environment = std::env::vars().count(); + eprintln!("fixture guest: WASI stderr"); + json!({ + "workspaceRoot": workspace.as_ref().is_some_and(|info| !info.root.is_empty()), + "cacheRoundTrip": cached == Some(probe), + "morphirLog": logged, + "wasiClock": clock, + "wasiRandomSeed": seeded.len() == 1, + "wasiArguments": arguments, + "wasiEnvironment": environment, + }) +} + +#[cfg(not(target_arch = "wasm32"))] +fn observe_host() -> Value { + json!({"native": true}) +} + +/// `morphir_log`, the fourth Morphir host function. The SDK at the pinned +/// revision does not declare it, so the fixture declares it as a guest would. +#[cfg(target_arch = "wasm32")] +mod log { + use morphir_extension_sdk::extism_pdk::{self, *}; + + #[host_fn] + extern "ExtismHost" { + fn morphir_log(level: String, message: String); + } + + pub fn send(level: &str, message: &str) -> bool { + unsafe { morphir_log(level.to_string(), message.to_string()).is_ok() } + } +} diff --git a/apps/morphir/scripts/extension-qualification-consumer.py b/apps/morphir/scripts/extension-qualification-consumer.py new file mode 100644 index 000000000..422d84065 --- /dev/null +++ b/apps/morphir/scripts/extension-qualification-consumer.py @@ -0,0 +1,31 @@ +"""Consumer check of the Python that the Python extension generated. + +Usage: python3 -I extension-qualification-consumer.py + +The generated directory holds the package of the extism-python fixture +(`acme/models.py` and `acme/rules.py`). The check compiles every generated file, +imports the rules module and calls its functions. It needs Python 3.12 or +later, because the generated models use `type` statements. +""" +from __future__ import annotations + +import importlib +import json +import sys +from pathlib import Path + +assert sys.version_info >= (3, 12), f"Python 3.12 or later is required, not {sys.version}" +generated = Path(sys.argv[1]).resolve() +files = sorted(generated.rglob("*.py")) +assert files, f"{generated} holds no Python files" +for file in files: + compile(file.read_text(), str(file), "exec") +sys.path.insert(0, str(generated)) +rules = importlib.import_module("acme.rules") +assert rules.choose(True, 5, 7) == 5 +assert rules.choose(False, 5, 7) == 7 +assert rules.twice_identity(9) == 9 +assert rules.apply_twice(lambda value: value + 1, 1) == 3 +assert rules.large_value() == 9007199254740993 +assert rules.pair(1.0, 2.0) == (1.0, 2.0) +print(json.dumps({"python": sys.version.split()[0], "files": [f.relative_to(generated).as_posix() for f in files], "consumer": True})) diff --git a/apps/morphir/scripts/extension-qualification.ts b/apps/morphir/scripts/extension-qualification.ts new file mode 100644 index 000000000..c01c995dc --- /dev/null +++ b/apps/morphir/scripts/extension-qualification.ts @@ -0,0 +1,839 @@ +// Qualification of published and fixture extensions on the CLI's worker host. +// +// node apps/morphir/scripts/extension-qualification.ts --suite --evidence +// --native --npm-entry +// [--python ] [--portable ] +// [--native-guest ] +// +// Suites: elm (task 7.1), python (7.2), moonbit-native and moonbit-portable (7.3), +// conformance (7.4). `scripts/extension-qualification.mbtx` builds the inputs and +// runs this file; run that script, not this file, from mise. +// +// Every downloaded file goes into a new, empty directory and is used only after +// its SHA-256 equals the pin in `apps/morphir/fixtures/qualification/published.json`. +// Each operation becomes one receipt row that names the extension, its version, +// the CLI, the platform, the operation, the result and the guest digest. The +// receipt is written to `/.json`; nothing is written to stdout. +// Progress goes to stderr. The exit status is 0 when every row passed or is a +// recorded known failure, and 1 otherwise. + +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { spawnSync, type SpawnSyncReturns } from 'node:child_process'; +import { + chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, + renameSync, rmSync, statSync, writeFileSync, +} from 'node:fs'; +import { delimiter, dirname, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parseArgs } from 'node:util'; + +const repository = realpathSync(fileURLToPath(new URL('../../../', import.meta.url))); +const fixtures = join(repository, 'apps/morphir/fixtures'); +const published = JSON.parse(readFileSync(join(fixtures, 'qualification/published.json'), 'utf8')); + +/** A SHA-256 digest as 64 lower-case hexadecimal digits. */ +type Sha256 = string & { readonly __sha256: unique symbol }; +/** A release platform, for example `aarch64-apple-darwin`. */ +type Platform = string & { readonly __platform: unique symbol }; +type CliName = 'native' | 'npm'; +type RowResult = 'passed' | 'failed' | 'known-failure'; + +const sha256 = (path: string) => createHash('sha256').update(readFileSync(path)).digest('hex') as Sha256; +const log = (message: string) => process.stderr.write(`${message}\n`); + +/** The release platform of this machine. */ +export function hostPlatform(): Platform { + const arch = ({ arm64: 'aarch64', x64: 'x86_64' } as Record)[process.arch]; + assert.ok(arch, `No release platform for ${process.arch}`); + if (process.platform === 'darwin') return `${arch}-apple-darwin` as Platform; + if (process.platform === 'win32') return `${arch}-pc-windows-msvc` as Platform; + const report = process.report?.getReport?.() as { header?: { glibcVersionRuntime?: string } } | undefined; + return `${arch}-unknown-linux-${report?.header?.glibcVersionRuntime ? 'gnu' : 'musl'}` as Platform; +} + +// --------------------------------------------------------------------------- +// Downloads + +const downloads = join(repository, '.dev/extension-qualification/downloads'); + +/** + * Returns a verified local copy of one pinned file. A file already verified in + * the cache directory named by its digest is hashed again and reused. Otherwise + * the file is downloaded into a new, empty directory and moved into the cache + * only when its SHA-256 equals the pin. + */ +function fetchPinned(url: string, filename: string, expected: Sha256): string { + const cached = join(downloads, expected, filename); + if (existsSync(cached) && sha256(cached) === expected) return realpathSync(cached); + mkdirSync(downloads, { recursive: true }); + const fresh = mkdtempSync(join(downloads, 'download-')); + const target = join(fresh, filename); + log(`download ${url}`); + const curl = spawnSync('curl', ['--fail', '--silent', '--show-error', '--location', '--proto', '=https', + '--proto-redir', '=https', '--retry', '3', '--connect-timeout', '30', '--max-time', '900', + '--max-filesize', String(512 * 1024 * 1024), '--output', target, url], { stdio: ['ignore', 'inherit', 'inherit'] }); + assert.equal(curl.status, 0, `download failed: ${url}`); + const actual = sha256(target); + if (actual !== expected) { + rmSync(fresh, { recursive: true, force: true }); + assert.fail(`${filename}: SHA-256 ${actual} differs from the pin ${expected}`); + } + rmSync(join(downloads, expected), { recursive: true, force: true }); + renameSync(fresh, join(downloads, expected)); + return realpathSync(cached); +} + +const releaseUrl = (repo: string, tag: string, filename: string) => + `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${filename}`; + +function fetchRelease(section: { repository: string; tag: string }, asset: { filename: string; sha256: string }) { + return fetchPinned(releaseUrl(section.repository, section.tag, asset.filename), asset.filename, asset.sha256 as Sha256); +} + +/** The shared MCK runner of the pinned finos/morphir release, unpacked in a new directory. */ +function mckRunner(platform: Platform): { runner: string; archiveSha256: Sha256 } { + const asset = published.mck.runners[platform]; + assert.ok(asset, `No pinned MCK runner for ${platform}`); + const archive = fetchRelease(published.mck, asset); + const unpacked = join(dirname(archive), 'unpacked'); + if (!existsSync(unpacked)) { + const fresh = mkdtempSync(join(dirname(archive), 'unpack-')); + // Windows: the system bsdtar reads .zip; a GNU tar earlier on PATH (Git Bash) does not. + const tarProgram = process.platform === 'win32' + ? join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'tar.exe') : 'tar'; + const tar = spawnSync(tarProgram, ['-xf', archive, '-C', fresh], { stdio: ['ignore', 'inherit', 'inherit'] }); + assert.equal(tar.status, 0, `cannot unpack ${archive}`); + renameSync(fresh, unpacked); + } + const name = process.platform === 'win32' ? 'morphir.exe' : 'morphir'; + const found = findFile(unpacked, name); + assert.ok(found, `${archive} holds no ${name}`); + return { runner: found, archiveSha256: asset.sha256 }; +} + +function findFile(root: string, name: string): string | undefined { + for (const entry of readdirSync(root, { withFileTypes: true })) { + const path = join(root, entry.name); + if (entry.isFile() && entry.name === name) return path; + if (entry.isDirectory()) { + const found = findFile(path, name); + if (found) return found; + } + } + return undefined; +} + +/** A shared MEP kit at the pinned finos/morphir revision. */ +function kit(name: 'guest-moonbit-bool' | 'guest-python-json'): { path: string; sha256: Sha256; value: any } { + const pin = published.mck.kits[name]; + const url = `https://raw.githubusercontent.com/${published.mck.repository}/${published.mck.sourceRevision}/${pin.path}`; + const path = fetchPinned(url, `${name}.json`, pin.sha256); + return { path, sha256: pin.sha256, value: JSON.parse(readFileSync(path, 'utf8')) }; +} + +// --------------------------------------------------------------------------- +// The two CLIs + +interface Cli { + name: CliName; + command: string; + prefix: string[]; + /** + * The environment of every run. On Unix the native CLI's PATH is one new, + * empty directory, so no `node` can be on it, wherever the host installs + * Node; the npm CLI's PATH is Node's directory, then `/usr/bin` and `/bin`. + */ + env: NodeJS.ProcessEnv; + build: { commit: string; dirty: boolean; programSha256: Sha256; entrySha256?: Sha256; node?: string }; +} + +function baseEnvironment(home: string, path: string[], isolated = false): NodeJS.ProcessEnv { + if (process.platform === 'win32') { + const root = process.env.SystemRoot ?? 'C:\\Windows'; + return { + SystemRoot: root, SystemDrive: process.env.SystemDrive ?? 'C:', ComSpec: join(root, 'System32', 'cmd.exe'), + PATHEXT: process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD', PATH: [...path, join(root, 'System32')].join(delimiter), + TEMP: home, TMP: home, USERPROFILE: home, APPDATA: home, LOCALAPPDATA: home, + }; + } + return { PATH: (isolated ? path : [...path, '/usr/bin', '/bin']).join(delimiter), HOME: home }; +} + +/** Checks that the native PATH is isolated (Unix: only empty directories) and says what the no-Node check found. */ +function nodeCheckDetail(env: NodeJS.ProcessEnv): string { + if (process.platform !== 'win32') { + for (const entry of (env.PATH ?? '').split(delimiter)) { + assert.deepEqual(readdirSync(entry), [], `the native PATH entry ${entry} is not an empty directory`); + } + } + const entries = (env.PATH ?? '').split(delimiter).map(entry => `${entry} (${existsSync(entry) ? readdirSync(entry).length : 0} entries)`); + const probe = process.platform === 'win32' ? 'where.exe node.exe' : "/bin/sh -c 'command -v node'"; + return `native PATH: ${entries.join(delimiter)}; ${probe} finds no node there and finds node on the npm CLI PATH`; +} + +/** Whether `node` can be found on the PATH of an environment. */ +function nodeVisible(env: NodeJS.ProcessEnv): boolean { + const probe = process.platform === 'win32' + ? spawnSync('where.exe', ['node.exe'], { env, encoding: 'utf8' }) + : spawnSync('/bin/sh', ['-c', 'command -v node'], { env, encoding: 'utf8' }); + return probe.status === 0; +} + +function gitBuild() { + const git = (args: string[]) => spawnSync('git', args, { cwd: repository, encoding: 'utf8' }).stdout.trim(); + return { commit: git(['rev-parse', 'HEAD']), dirty: git(['status', '--porcelain']) !== '' }; +} + +function clis(native: string, npmEntry: string, home: string): Cli[] { + const build = gitBuild(); + // The native CLI needs no program from PATH: it starts its workers by path. + const emptyPath = join(home, '..', 'native-path'); + mkdirSync(emptyPath); + return [ + { name: 'native', command: native, prefix: [], env: baseEnvironment(home, [emptyPath], true), + build: { ...build, programSha256: sha256(native) } }, + { name: 'npm', command: process.execPath, prefix: [npmEntry], env: baseEnvironment(home, [dirname(process.execPath)]), + build: { ...build, programSha256: sha256(process.execPath), entrySha256: sha256(npmEntry), node: process.version } }, + ]; +} + +interface Run { status: number | null; stdout: string; stderr: string; report?: any } + +function run(cli: Cli, args: string[], cwd: string, timeout = 240000): Run { + const child: SpawnSyncReturns = spawnSync(cli.command, [...cli.prefix, ...args], + { cwd, env: cli.env, encoding: 'utf8', timeout, killSignal: 'SIGKILL', maxBuffer: 64 * 1024 * 1024 }); + const result: Run = { status: child.status, stdout: child.stdout ?? '', stderr: child.stderr ?? '' }; + if (args.includes('--json')) { + try { result.report = JSON.parse(result.stdout); } catch { /* the assertion below names it */ } + assert.ok(result.report, `${cli.name} ${args.join(' ')}: stdout is not one JSON document:\n${result.stdout}\n${result.stderr}`); + } + return result; +} + +/** Runs one command in JSON mode and checks its exit status and report. */ +function command(cli: Cli, args: string[], cwd: string, expected: number): any { + const result = run(cli, [...args, '--json'], cwd); + const tail = `${result.stdout}\n${result.stderr.slice(-4000)}`; + assert.equal(result.status, expected, `${cli.name} ${args.join(' ')} exited ${result.status}:\n${tail}`); + assert.equal(result.report.profile, 'morphir/command-result', tail); + assert.equal(result.report.contractVersion, '0.1.0', tail); + assert.equal(result.report.exitCode, expected, tail); + assert.equal(result.report.state, expected === 0 ? 'succeeded' : 'failed', tail); + return result.report; +} + +// --------------------------------------------------------------------------- +// Receipts + +interface Row { + extension: string; + version: string; + cli: CliName | 'mck'; + platform: Platform; + operation: string; + result: RowResult; + guestSha256: Sha256; + descriptorSha256?: Sha256; + outputs?: { path: string; bytes: number; sha256: Sha256 }[]; + detail?: string; + finding?: string; +} + +/** + * A failure that is recorded and explained, not hidden. The row is + * `known-failure` while the operation fails with a message that matches; an + * operation that passes instead fails the row, so the expectation is removed. + */ +interface KnownFailure { match: RegExp; finding: string } + +class Ledger { + readonly rows: Row[] = []; + readonly platform: Platform; + constructor(platform: Platform) { this.platform = platform; } + + step(base: Omit, body: () => Partial | void, known?: KnownFailure): boolean { + const row: Row = { ...base, platform: this.platform, result: 'passed' }; + try { + Object.assign(row, body() ?? {}); + if (known) { + row.result = 'failed'; + row.detail = `passed, but it is recorded as a known failure (${known.finding}); remove the expectation`; + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + row.detail = message.slice(0, 4000); + if (known && known.match.test(message)) { + row.result = 'known-failure'; + row.finding = known.finding; + } else row.result = 'failed'; + } + this.rows.push(row); + log(`${row.result.padEnd(13)} ${row.extension} ${row.version} ${row.cli} ${row.operation}${row.result === 'passed' ? '' : `: ${row.finding ?? row.detail?.split('\n')[0]}`}`); + return row.result === 'passed'; + } +} + +function outputs(directory: string, files: { path: string; bytes: number; sha256: string }[]) { + for (const file of files) assert.equal(sha256(join(directory, file.path)), file.sha256, `${file.path} digest`); + return files.map(file => ({ path: file.path, bytes: file.bytes, sha256: file.sha256 as Sha256 })); +} + +function treeDigests(root: string): Record { + const result: Record = {}; + const walk = (directory: string) => { + for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + const path = join(directory, entry.name); + if (entry.isDirectory()) walk(path); + else result[relative(root, path).split('\\').join('/')] = sha256(path); + } + }; + walk(root); + return result; +} + +function writePin(path: string, pin: Record) { + writeFileSync(path, `${JSON.stringify(pin, null, 2)}\n`); + return path; +} + +function processPin(artifact: string, platform: Platform, origin: string, descriptor?: string) { + return { + contractVersion: '0.2.0', + artifact: { path: artifact, sha256: sha256(artifact) }, + runtime: { kind: 'process', platform, args: [] }, + ...(descriptor ? { descriptor: { path: descriptor, sha256: sha256(descriptor) } } : {}), + origin, + }; +} + +function wasmPin(artifact: string, abi: string, origin: string, descriptor?: string) { + return { + contractVersion: '0.2.0', + artifact: { path: artifact, sha256: sha256(artifact) }, + runtime: { kind: 'wasm', abi }, + ...(descriptor ? { descriptor: { path: descriptor, sha256: sha256(descriptor) } } : {}), + origin, + }; +} + +/** Checks the describe report against the expected identity and worker kind. */ +function described(report: any, id: string, version: string, kind: 'process' | 'wasm') { + assert.equal(report.command, 'extension.describe'); + assert.equal(report.result.worker.kind, kind); + assert.deepEqual(report.result.extension, { id, version }); + return report.result; +} + +// --------------------------------------------------------------------------- +// Suites + +interface Context { + ledger: Ledger; + platform: Platform; + clis: Cli[]; + scratch: string; + python?: string; + portable?: string; + nativeGuest?: string; + notes: Record; +} + +/** + * The Elm extensions (task 7.1): the native executables of `v0.4.1` and of + * elm-classic `v0.5.1` as process workers, and the `v0.5.0-preview.2` WASM + * module (`morphir-bytes-v1`, the model line). + */ +function elmSuite(context: Context) { + elmProcessGuest(context, published.elm, 'morphir-elm', { + match: /Invalid morphir\.frontend\.compile parameters/, + finding: 'Elm 0.4.1 requires package.exposedModules (cli2/mep/protocol.ts); the MEP Smithy model makes it optional and the host, which conforms, does not send it; follow-up bead morphir-ukvn.5.64.36.16 (finos/morphir-elm), fixed in elm-classic 0.5.1', + }); + elmProcessGuest(context, published.elmClassic, 'morphir-elm-classic'); + elmWasmGuest(context); +} + +/** One Elm release of native executables: describe, the no-Node check and compile of the fixture. */ +function elmProcessGuest(context: Context, elm: any, id: string, known?: KnownFailure) { + const { ledger, platform } = context; + const asset = elm.executables[platform]; + assert.ok(asset, `The Elm extension ${elm.version} has no executable for ${platform}`); + const executable = fetchRelease(elm, asset); + if (process.platform !== 'win32') chmodSync(executable, 0o755); + const descriptor = fetchRelease(elm, elm.descriptor); + const pin = writePin(join(context.scratch, `${id}-${elm.version}-pin.json`), + processPin(executable, platform, `${elm.repository} ${elm.tag}`, descriptor)); + const base = { extension: id, version: elm.version, guestSha256: asset.sha256 as Sha256, descriptorSha256: elm.descriptor.sha256 as Sha256 }; + const compiled: Record> = {}; + for (const cli of context.clis) { + const directory = mkdtempSync(join(context.scratch, `${id}-${cli.name}-`)); + ledger.step({ ...base, cli: cli.name, operation: 'describe' }, () => { + const result = described(command(cli, ['extension', 'describe', '--extension-pin', pin], directory, 0), id, elm.version, 'process'); + assert.equal(result.worker.descriptorSha256, elm.descriptor.sha256); + assert.deepEqual(result.claims.protocolVersions, ['0.1']); + return { detail: `claims ${JSON.stringify(result.claims.languages)}` }; + }); + if (cli.name === 'native') { + ledger.step({ ...base, cli: cli.name, operation: 'environment-without-node' }, () => { + assert.equal(nodeVisible(cli.env), false, `node is on the native run's PATH ${cli.env.PATH}`); + // The same probe finds Node on the npm CLI's PATH, so it can see Node. + const npm = context.clis.find(other => other.name === 'npm'); + assert.ok(!npm || nodeVisible(npm.env), 'the probe does not find Node even on the npm CLI PATH'); + return { detail: nodeCheckDetail(cli.env) }; + }); + } + ledger.step({ ...base, cli: cli.name, operation: 'compile' }, () => { + const report = command(cli, ['extension', 'compile', join(fixtures, 'elm-extension/v1/src'), '--package', 'acme/rules', + '--output', 'model', '--extension-pin', pin], directory, 0); + assert.equal(report.result.protocolVersion, '0.1'); + const ir = JSON.parse(readFileSync(join(directory, 'model/morphir-ir.json'), 'utf8')); + assert.equal(ir.formatVersion, 3); + compiled[cli.name] = treeDigests(join(directory, 'model')); + return { outputs: outputs(join(directory, 'model'), report.result.output.files) }; + }, known); + } + const { native: nativeOutputs, npm: npmOutputs } = compiled; + if (nativeOutputs && npmOutputs) { + ledger.step({ ...base, cli: 'npm', operation: 'same-output-as-native' }, () => assert.deepEqual(npmOutputs, nativeOutputs)); + } +} + +/** + * The Elm extension as one WASM module (`morphir-bytes-v1`): describe, compile + * on the model line, generate Elm, and recompile the generated sources to the + * same model. It names modules under the package name, so its fixture is + * `Acme.Rules.Main` in the package `Acme.Rules`. + */ +function elmWasmGuest(context: Context) { + const { ledger } = context; + const elm = published.elmWasm; + const module = fetchRelease(elm, elm.module); + const descriptor = fetchRelease(elm, elm.descriptor); + const pin = writePin(join(context.scratch, `morphir-elm-${elm.version}-pin.json`), + wasmPin(module, 'morphir-bytes-v1', `${elm.repository} ${elm.tag}`, descriptor)); + const base = { extension: 'morphir-elm', version: elm.version, guestSha256: elm.module.sha256 as Sha256, descriptorSha256: elm.descriptor.sha256 as Sha256 }; + const results: Record>> = {}; + for (const cli of context.clis) { + const directory = mkdtempSync(join(context.scratch, `morphir-elm-wasm-${cli.name}-`)); + const mine: Record> = {}; + results[cli.name] = mine; + ledger.step({ ...base, cli: cli.name, operation: 'describe' }, () => { + const result = described(command(cli, ['extension', 'describe', '--extension-pin', pin], directory, 0), 'morphir-elm', elm.version, 'wasm'); + assert.equal(result.worker.runtime.abi, 'morphir-bytes-v1'); + assert.equal(result.worker.descriptorSha256, elm.descriptor.sha256); + return { detail: `operations ${result.claims.operations.join(', ')}` }; + }); + const compiled = ledger.step({ ...base, cli: cli.name, operation: 'compile' }, () => { + const report = command(cli, ['extension', 'compile', join(fixtures, 'elm-extension/v1/package'), '--package', 'Acme.Rules', + '--output', 'model', '--extension-pin', pin], directory, 0); + assert.equal(report.result.protocolVersion, '0.2.0-draft.1'); + assert.equal(report.result.negotiated.tuple.codec, 'morphir-json'); + mine.model = treeDigests(join(directory, 'model')); + return { outputs: outputs(join(directory, 'model'), report.result.output.files) }; + }); + if (!compiled) continue; + ledger.step({ ...base, cli: cli.name, operation: 'generate-and-recompile' }, () => { + const report = command(cli, ['extension', 'generate', 'model/model-envelope.json', '--output', 'generated', + '--extension-pin', pin], directory, 0); + assert.ok(existsSync(join(directory, 'generated/src/Acme/Rules/Main.elm'))); + command(cli, ['extension', 'compile', 'generated/src', '--package', 'Acme.Rules', '--output', 'again', + '--extension-pin', pin], directory, 0); + assert.deepEqual(readFileSync(join(directory, 'again/model.json')), readFileSync(join(directory, 'model/model.json'))); + mine.generated = treeDigests(join(directory, 'generated')); + return { outputs: outputs(join(directory, 'generated'), report.result.output.files) }; + }); + } + const { native: nativeOutputs, npm: npmOutputs } = results; + if (nativeOutputs && npmOutputs) { + ledger.step({ ...base, cli: 'npm', operation: 'same-output-as-native' }, () => assert.deepEqual(npmOutputs, nativeOutputs)); + } +} + +/** Python extension 0.4.1 and 0.4.2: extism-kernel-v1 WASM workers (task 7.2). */ +function pythonSuite(context: Context) { + const { ledger } = context; + const python = context.python; + for (const version of ['0.4.1', '0.4.2']) { + const release = published.python.releases[version]; + const section = { repository: published.python.repository, tag: release.tag }; + const module = fetchRelease(section, release.module); + const descriptor = fetchRelease(section, release.descriptor); + const pin = writePin(join(context.scratch, `python-${version}-pin.json`), + wasmPin(module, 'extism-kernel-v1', `${published.python.repository} ${release.tag}`, descriptor)); + const base = { extension: 'morphir-python', version, guestSha256: release.module.sha256 as Sha256, descriptorSha256: release.descriptor.sha256 as Sha256 }; + const results: Record>> = {}; + for (const cli of context.clis) { + const directory = mkdtempSync(join(context.scratch, `python-${version}-${cli.name}-`)); + cpSync(join(fixtures, 'extism-python/v1/acme'), join(directory, 'src/acme'), { recursive: true }); + cpSync(join(fixtures, 'extism-python/v1/single'), join(directory, 'single'), { recursive: true }); + const mine: Record> = {}; + results[cli.name] = mine; + ledger.step({ ...base, cli: cli.name, operation: 'describe' }, () => { + const result = described(command(cli, ['extension', 'describe', '--extension-pin', pin], directory, 0), 'morphir-python', version, 'wasm'); + assert.equal(result.worker.runtime.abi, 'extism-kernel-v1'); + assert.equal(result.worker.descriptorSha256, release.descriptor.sha256); + return { detail: `multiDocument ${result.claims.frontendFlags?.multiDocument ?? false}` }; + }); + if (version === '0.4.1') { + for (const ir of ['3', '4']) { + ledger.step({ ...base, cli: cli.name, operation: `compile-single-document-ir${ir}` }, () => { + const report = command(cli, ['extension', 'compile', 'single/rules.py', '--package', 'acme/rules', '--ir-version', ir, + '--output', `single-v${ir}`, '--extension-pin', pin], directory, 0); + assert.equal(JSON.parse(readFileSync(join(directory, `single-v${ir}/morphir-ir.json`), 'utf8')).formatVersion, Number(ir)); + mine[`single-v${ir}`] = treeDigests(join(directory, `single-v${ir}`)); + return { outputs: outputs(join(directory, `single-v${ir}`), report.result.output.files) }; + }); + } + ledger.step({ ...base, cli: cli.name, operation: 'refuse-multi-document' }, () => { + const report = command(cli, ['extension', 'compile', 'src', '--package', 'acme/rules', '--ir-version', '3', + '--output', 'multi', '--extension-pin', pin], directory, 2); + assert.equal(report.error.code, 'extension.capability_unavailable'); + assert.match(report.error.message, /multiDocument/); + assert.equal(existsSync(join(directory, 'multi')), false); + assert.equal(existsSync(join(directory, 'multi.morphir-partial')), false); + return { detail: report.error.message }; + }); + continue; + } + for (const ir of ['3', '4']) { + const model = `model-v${ir}`, generated = `generated-v${ir}`, again = `again-v${ir}`; + const compiled = ledger.step({ ...base, cli: cli.name, operation: `compile-ir${ir}` }, () => { + const report = command(cli, ['extension', 'compile', 'src', '--package', 'acme/rules', '--ir-version', ir, + '--output', model, '--extension-pin', pin], directory, 0); + assert.equal(report.result.protocolVersion, '0.1'); + assert.equal(JSON.parse(readFileSync(join(directory, model, 'morphir-ir.json'), 'utf8')).formatVersion, Number(ir)); + mine[model] = treeDigests(join(directory, model)); + return { outputs: outputs(join(directory, model), report.result.output.files) }; + }); + if (!compiled) continue; + const generatedOk = ledger.step({ ...base, cli: cli.name, operation: `generate-ir${ir}` }, () => { + // MEP 0.1 generate takes the IR version from formatVersion and sends no options.irVersion. + const report = command(cli, ['extension', 'generate', join(model, 'morphir-ir.json'), '--output', generated, + '--extension-pin', pin], directory, 0); + mine[generated] = treeDigests(join(directory, generated)); + return { outputs: outputs(join(directory, generated), report.result.output.files) }; + }); + if (!generatedOk) continue; + ledger.step({ ...base, cli: cli.name, operation: `consumer-ir${ir}` }, () => { + assert.ok(python, 'no Python 3.12 or later for the consumer check; pass --python'); + const check = spawnSync(python, ['-I', join(repository, 'apps/morphir/scripts/extension-qualification-consumer.py'), + join(directory, generated)], { encoding: 'utf8', timeout: 60000 }); + assert.equal(check.status, 0, `${check.stdout}\n${check.stderr}`); + return { detail: check.stdout.trim() }; + }); + ledger.step({ ...base, cli: cli.name, operation: `recompile-generated-ir${ir}` }, () => { + command(cli, ['extension', 'compile', generated, '--package', 'acme/rules', '--ir-version', ir, + '--output', again, '--extension-pin', pin], directory, 0); + assert.deepEqual(JSON.parse(readFileSync(join(directory, again, 'morphir-ir.json'), 'utf8')), + JSON.parse(readFileSync(join(directory, model, 'morphir-ir.json'), 'utf8'))); + }); + ledger.step({ ...base, cli: cli.name, operation: `refuse-ir-version-mismatch-ir${ir}` }, () => { + const report = command(cli, ['extension', 'generate', join(model, 'morphir-ir.json'), '--ir-version', ir === '3' ? '4' : '3', + '--output', `mismatch-v${ir}`, '--extension-pin', pin], directory, 2); + assert.equal(report.error.code, 'extension.ir_version_mismatch'); + assert.equal(existsSync(join(directory, `mismatch-v${ir}`)), false); + }); + } + } + const { native: nativeOutputs, npm: npmOutputs } = results; + if (nativeOutputs && npmOutputs) { + ledger.step({ ...base, cli: 'npm', operation: 'same-output-as-native' }, () => { + assert.deepEqual(npmOutputs, nativeOutputs); + return { detail: `${Object.keys(nativeOutputs).length} output directories` }; + }); + } + } +} + +/** The MoonBit guest, native (process) or portable (morphir-bytes-v1), with the shared kit (task 7.3). */ +function moonbitSuite(context: Context, flavour: 'native' | 'portable') { + const { ledger, platform } = context; + const shared = kit('guest-moonbit-bool'); + const artifact = flavour === 'native' ? context.nativeGuest : context.portable && join(context.portable, 'model.wasm'); + assert.ok(artifact && existsSync(artifact), `No MoonBit ${flavour} guest; pass --${flavour === 'native' ? 'native-guest' : 'portable'}`); + const guestSha256 = sha256(artifact); + const pin = writePin(join(context.scratch, `moonbit-${flavour}-pin.json`), flavour === 'native' + ? processPin(artifact, platform, 'finos/morphir-moonbit apps/morphir-extension (local build)') + : wasmPin(artifact, 'morphir-bytes-v1', 'finos/morphir-moonbit mise run package:mep-extension (local build)')); + const base = { extension: 'morphir-moonbit', version: '0.1.0', guestSha256 }; + const source = shared.value.source; + const document = source.sources.documents[0]; + const results: Record>> = {}; + for (const cli of context.clis) { + const directory = mkdtempSync(join(context.scratch, `moonbit-${flavour}-${cli.name}-`)); + mkdirSync(join(directory, 'src')); + writeFileSync(join(directory, 'src/main.mbt'), document.text); + writeFileSync(join(directory, 'json-envelope.json'), JSON.stringify(jsonEnvelope(shared.value.plainIr))); + const mine: Record> = {}; + results[cli.name] = mine; + ledger.step({ ...base, cli: cli.name, operation: 'describe' }, () => { + const result = described(command(cli, ['extension', 'describe', '--extension-pin', pin], directory, 0), 'morphir-moonbit', '0.1.0', flavour === 'native' ? 'process' : 'wasm'); + if (flavour === 'portable') assert.equal(result.worker.runtime.abi, 'morphir-bytes-v1'); + }); + if (cli.name === 'native' && flavour === 'native') { + ledger.step({ ...base, cli: cli.name, operation: 'environment-without-node' }, () => { + assert.equal(nodeVisible(cli.env), false); + const npm = context.clis.find(other => other.name === 'npm'); + assert.ok(!npm || nodeVisible(npm.env), 'the probe does not find Node even on the npm CLI PATH'); + return { detail: nodeCheckDetail(cli.env) }; + }); + } + const compiled = ledger.step({ ...base, cli: cli.name, operation: 'compile-kit-source' }, () => { + const report = command(cli, ['extension', 'compile', 'src/main.mbt', '--package', source.package.name, '--output', 'model', + '--extension-pin', pin], directory, 0); + assert.equal(report.result.protocolVersion, '0.2.0-draft.1'); + assert.equal(report.result.negotiated.tuple.codec, 'ion-binary'); + mine.model = treeDigests(join(directory, 'model')); + return { outputs: outputs(join(directory, 'model'), report.result.output.files) }; + }); + if (compiled) { + const generated = ledger.step({ ...base, cli: cli.name, operation: 'generate' }, () => { + const report = command(cli, ['extension', 'generate', 'model/model-envelope.json', '--output', 'generated', + '--extension-pin', pin], directory, 0); + assert.deepEqual(readFileSync(join(directory, 'generated/mep/source-model.ionb')), readFileSync(join(directory, 'model/model.ionb'))); + mine.generated = treeDigests(join(directory, 'generated')); + return { outputs: outputs(join(directory, 'generated'), report.result.output.files) }; + }); + if (generated) { + ledger.step({ ...base, cli: cli.name, operation: 'kit-consumer' }, () => { + const stdout = moonbitConsumer(directory, join(directory, 'generated'), shared.value); + return { detail: stdout.trim() }; + }); + } + } + ledger.step({ ...base, cli: cli.name, operation: 'generate-kit-plain-ir' }, () => { + const report = command(cli, ['extension', 'generate', 'json-envelope.json', '--output', 'json-generated', + '--extension-pin', pin], directory, 0); + for (const [name, text] of Object.entries(shared.value.expectedJsonArtifacts as Record)) { + assert.equal(readFileSync(join(directory, 'json-generated', name), 'utf8'), text, `${name} differs from the kit`); + } + return { outputs: outputs(join(directory, 'json-generated'), report.result.output.files) }; + }); + } + const { native: nativeOutputs, npm: npmOutputs } = results; + if (nativeOutputs && npmOutputs) { + ledger.step({ ...base, cli: 'npm', operation: 'same-output-as-native' }, () => assert.deepEqual(npmOutputs, nativeOutputs)); + } + mckGuestRun(context, flavour, artifact, shared, base); +} + +/** A Morphir JSON model envelope around the kit's independent plain IR. */ +function jsonEnvelope(plainIr: unknown) { + const bytes = Buffer.from(JSON.stringify(plainIr)); + const digest = createHash('sha256').update(bytes).digest('hex'); + return { + envelopeVersion: '0.1.0-draft.1', modelId: 'urn:morphir:mck:bool', revision: `sha256:${digest}`, irVersion: '4.0.0', + profile: 'morphir-ir', + payload: { codec: 'morphir-json', layout: 'single-document', decodedByteLength: bytes.length, sha256: digest, content: bytes.toString('base64') }, + }; +} + +/** Builds the generated library with the kit's consumer source and checks the kit's expected stdout. */ +function moonbitConsumer(directory: string, generated: string, kitValue: any): string { + const consumer = join(directory, 'consumer'); + mkdirSync(consumer); + for (const [name, text] of Object.entries(kitValue.consumer.files as Record)) writeFileSync(join(consumer, name), text); + writeFileSync(join(directory, 'moon.work'), `members = ${JSON.stringify([generated, consumer, join(repository, 'pkgs/morphir-sdk')])}\n`); + const moon = spawnSync('moon', ['-C', directory, 'run', '--target', 'native', 'consumer'], { encoding: 'utf8', timeout: 600000 }); + assert.equal(moon.status, 0, moon.stderr.slice(-4000)); + assert.equal(moon.stdout, kitValue.consumer.expectedStdout); + return moon.stdout; +} + +/** + * Runs the shared MEP kit against the guest. The native guest is its own stdio + * host. The portable guest is served by the native CLI's internal worker mode, + * `morphir extension worker`, with the CLI's own build identity. + */ +function mckGuestRun(context: Context, flavour: 'native' | 'portable', artifact: string, + shared: { path: string; sha256: Sha256 }, base: { extension: string; version: string; guestSha256: Sha256 }) { + const { ledger, platform } = context; + const native = context.clis.find(cli => cli.name === 'native')!; + const evidence = mkdtempSync(join(context.scratch, `mck-${flavour}-`)); + ledger.step({ ...base, cli: 'mck', operation: `shared-kit-${flavour === 'native' ? 'direct' : 'native-cli-worker-mode'}` }, () => { + const { runner, archiveSha256 } = mckRunner(platform); + const report = join(evidence, 'report.json'); + const args = ['--no-banner', 'mck', 'mep', 'guest-run', '--kit', shared.path, '--expect-kit-sha256', shared.sha256, '--report', report]; + if (flavour === 'native') args.push('--host', artifact, '--identity-file', artifact); + else { + const help = run(native, ['extension', 'worker', '--help', '--json'], evidence); + const identity = JSON.parse(help.stdout).result.buildIdentity; + assert.match(identity, /^[0-9a-f]{64}$/); + args.push('--host', native.command); + for (const value of ['extension', 'worker', '--abi', 'morphir-bytes-v1', '--module', artifact, '--build-identity', identity, + '--request-bytes', String(4 * 1024 * 1024), '--response-bytes', String(16 * 1024 * 1024)]) args.push('--host-arg', value); + args.push('--identity-file', native.command, '--identity-file', artifact); + } + const environment = process.platform === 'win32' ? { ...native.env } : { PATH: '', MOON_HOME: '', HOME: evidence }; + const guestRun = spawnSync(runner, args, { cwd: evidence, env: environment, encoding: 'utf8', timeout: 600000 }); + const observed = existsSync(report) ? JSON.parse(readFileSync(report, 'utf8')) : undefined; + const cases = observed?.records?.map((record: { caseId: string }) => record.caseId) ?? []; + const detail = `runner ${published.mck.tag} archive ${archiveSha256}; kit ${shared.sha256}; ${cases.length} cases`; + assert.equal(guestRun.status, 0, `${detail}; ${observed?.error ?? guestRun.stderr.slice(-2000)}; cases ${cases.join(', ')}`); + const recheck = spawnSync(runner, ['--no-banner', 'mck', 'mep', 'report-check', '--kit', shared.path, report], + { cwd: evidence, env: environment, encoding: 'utf8', timeout: 600000 }); + assert.equal(recheck.status, 0, recheck.stderr.slice(-2000)); + assert.equal(observed.qualified, true); + return { detail, outputs: [{ path: 'report.json', bytes: statSync(report).size, sha256: sha256(report) }] }; + }, { + match: /moonbit_frontend\.unsupported_profile|compile result is not a clean successful main module|frontend source profile changed/, + finding: 'the shared kit asks for frontend profile moonbit-model-bool-library-v1, which the MoonBit guest retired in #181, so case compile-ion fails; follow-up bead morphir-ukvn.5.64.36.17 (finos/morphir kit)', + }); +} + +/** The SDK fixture guest: local conformance check, no shared kit profile fits it (task 7.4). */ +function conformanceSuite(context: Context) { + const { ledger } = context; + const root = join(fixtures, 'sdk-guest'); + const receipt = JSON.parse(readFileSync(join(root, 'build-receipt.json'), 'utf8')); + const artifact = join(root, receipt.artifact.path); + const base = { extension: 'morphir-sdk-fixture', version: '0.1.0', guestSha256: sha256(artifact) }; + context.notes.sharedKit = 'No shared MEP kit profile fits this guest: the shared kits qualify the MoonBit Boolean library and Python JSON library profiles, and each names its own extension. A generic kit profile is follow-up bead morphir-ukvn.5.64.36.15.'; + context.notes.hostChange = 'None: the guest runs on the worker host as it is on main.'; + ledger.step({ ...base, cli: 'native', operation: 'build-receipt' }, () => { + assert.equal(base.guestSha256, receipt.artifact.sha256, 'the stored module differs from its build receipt'); + for (const [path, digest] of Object.entries(receipt.sources as Record)) { + assert.equal(sha256(join(root, path)), digest, `${path} differs from the build receipt`); + } + return { detail: `${receipt.sdk.crate} ${receipt.sdk.revision}; ${receipt.toolchain.rustc}; ${receipt.toolchain.target}` }; + }); + ledger.step({ ...base, cli: 'native', operation: 'imports' }, () => { + const imports = wasmImports(readFileSync(artifact)); + assert.deepEqual(imports, receipt.imports); + const modules = [...new Set(imports.map(name => name.slice(0, name.lastIndexOf('.'))))].sort(); + assert.deepEqual(modules, ['extism:host/env', 'extism:host/user', 'wasi_snapshot_preview1']); + for (const name of ['morphir_get_workspace_info', 'morphir_cache_ir', 'morphir_get_cached_ir', 'morphir_log']) { + assert.ok(imports.includes(`extism:host/user.${name}`), name); + } + return { detail: `${imports.filter(name => name.startsWith('wasi_')).length} WASI and 4 Morphir host function imports` }; + }); + const pin = writePin(join(context.scratch, 'sdk-fixture-pin.json'), + wasmPin(artifact, 'extism-kernel-v1', 'finos/morphir-moonbit apps/morphir/fixtures/sdk-guest')); + const results: Record>> = {}; + for (const cli of context.clis) { + const directory = mkdtempSync(join(context.scratch, `sdk-${cli.name}-`)); + mkdirSync(join(directory, 'src')); + writeFileSync(join(directory, 'src/rules.flags'), '# fixture\nready = true\nblockedNow = false\n'); + writeFileSync(join(directory, 'src/extra.flags'), 'enabled = true\n'); + writeFileSync(join(directory, 'src/ignored.txt'), 'not a claimed source file\n'); + const mine: Record> = {}; + results[cli.name] = mine; + ledger.step({ ...base, cli: cli.name, operation: 'describe-classify' }, () => { + const result = described(command(cli, ['extension', 'describe', '--extension-pin', pin], directory, 0), 'morphir-sdk-fixture', '0.1.0', 'wasm'); + assert.equal(result.worker.runtime.abi, 'extism-kernel-v1'); + assert.deepEqual(result.claims.languages, [{ id: 'morphir-fixture-flags', fileExtensions: ['.flags'] }]); + return { detail: `operations ${result.claims.operations.join(', ')}` }; + }); + ledger.step({ ...base, cli: cli.name, operation: 'compile-claimed-sources' }, () => { + const report = command(cli, ['extension', 'compile', 'src', '--package', 'acme/flags', '--output', 'model', + '--extension-pin', pin], directory, 0); + const host = report.diagnostics.find((d: { code?: string }) => d.code === 'fixture.host'); + assert.ok(host, 'the guest reported no host observation'); + assert.deepEqual(JSON.parse(host.message), { + cacheRoundTrip: true, morphirLog: true, wasiArguments: 0, wasiClock: true, wasiEnvironment: 0, wasiRandomSeed: true, workspaceRoot: true, + }); + const ir = JSON.parse(readFileSync(join(directory, 'model/morphir-ir.json'), 'utf8')); + const modules = ir.distribution[3].modules.map((module: any) => module[0]); + assert.deepEqual(modules, [[['extra']], [['rules']]], 'the .txt file is not a claimed source'); + mine.model = treeDigests(join(directory, 'model')); + return { detail: host.message, outputs: outputs(join(directory, 'model'), report.result.output.files) }; + }); + ledger.step({ ...base, cli: cli.name, operation: 'generate-and-recompile' }, () => { + const report = command(cli, ['extension', 'generate', 'model/morphir-ir.json', '--output', 'generated', + '--extension-pin', pin], directory, 0); + assert.equal(readFileSync(join(directory, 'generated/rules.flags'), 'utf8'), 'ready = true\nblocked_now = false\n'); + command(cli, ['extension', 'compile', 'generated', '--package', 'acme/flags', '--output', 'again', '--extension-pin', pin], directory, 0); + assert.deepEqual(readFileSync(join(directory, 'again/morphir-ir.json')), readFileSync(join(directory, 'model/morphir-ir.json'))); + mine.generated = treeDigests(join(directory, 'generated')); + return { outputs: outputs(join(directory, 'generated'), report.result.output.files) }; + }); + ledger.step({ ...base, cli: cli.name, operation: 'guest-diagnostics' }, () => { + writeFileSync(join(directory, 'broken.flags'), 'ready = maybe\n'); + const report = command(cli, ['extension', 'compile', 'broken.flags', '--package', 'acme/flags', '--output', 'broken', + '--extension-pin', pin], directory, 1); + assert.equal(report.error.code, 'extension.operation_unsuccessful'); + assert.ok(report.diagnostics.some((d: { code?: string }) => d.code === 'fixture.syntax')); + assert.equal(existsSync(join(directory, 'broken')), false); + }); + } + const { native: nativeOutputs, npm: npmOutputs } = results; + if (nativeOutputs && npmOutputs) { + ledger.step({ ...base, cli: 'npm', operation: 'same-output-as-native' }, () => assert.deepEqual(npmOutputs, nativeOutputs)); + } +} + +/** The names of the function imports of a WASM module, `module.name`, in order. */ +export function wasmImports(bytes: Buffer): string[] { + assert.deepEqual([...bytes.subarray(0, 4)], [0, 0x61, 0x73, 0x6d], 'not a WASM module'); + let offset = 8; + const leb = () => { + let result = 0, shift = 0, byte = 0; + do { byte = bytes[offset++]!; result += (byte & 0x7f) * 2 ** shift; shift += 7; } while (byte & 0x80); + return result; + }; + const text = () => { const length = leb(); const value = bytes.subarray(offset, offset + length).toString('utf8'); offset += length; return value; }; + const names: string[] = []; + while (offset < bytes.length) { + const id = bytes[offset++]; + const size = leb(); + const end = offset + size; + if (id === 2) { + for (let count = leb(); count > 0; count--) { + const module = text(), name = text(); + assert.equal(bytes[offset++], 0, `${module}.${name} is not a function import`); + leb(); + names.push(`${module}.${name}`); + } + } + offset = end; + } + return names; +} + +// --------------------------------------------------------------------------- + +const suites = { + elm: elmSuite, python: pythonSuite, conformance: conformanceSuite, + 'moonbit-native': (context: Context) => moonbitSuite(context, 'native'), + 'moonbit-portable': (context: Context) => moonbitSuite(context, 'portable'), +} as const; +type SuiteName = keyof typeof suites; + +export function main(args: string[]) { + const { values } = parseArgs({ + args, strict: true, allowPositionals: false, + options: Object.fromEntries(['suite', 'evidence', 'native', 'npm-entry', 'python', 'portable', 'native-guest'] + .map(name => [name, { type: 'string' as const }])), + }); + const suite = values.suite as SuiteName; + assert.ok(suite in suites, `--suite must be one of ${Object.keys(suites).join(', ')}`); + const { evidence, native, 'npm-entry': npmEntry } = values; + assert.ok(evidence && native && npmEntry, '--evidence, --native and --npm-entry are required'); + mkdirSync(evidence, { recursive: true }); + const platform = hostPlatform(); + const scratch = realpathSync(mkdtempSync(join(realpathSync(evidence), `${suite}-`))); + const home = join(scratch, 'home'); + mkdirSync(home); + const ledger = new Ledger(platform); + const context: Context = { + ledger, platform, scratch, clis: clis(realpathSync(native), realpathSync(npmEntry), home), + python: values.python, portable: values.portable, nativeGuest: values['native-guest'], notes: {}, + }; + let error: string | undefined; + try { suites[suite](context); } catch (failure) { error = failure instanceof Error ? failure.message : String(failure); log(error); } + const failed = error !== undefined || ledger.rows.some(row => row.result === 'failed'); + const receipt = { + profile: 'morphir/extension-qualification', contractVersion: '0.1.0', suite, + state: failed ? 'failed' : 'passed', platform, + cli: Object.fromEntries(context.clis.map(cli => [cli.name, cli.build])), + ...(error ? { error } : {}), notes: context.notes, rows: ledger.rows, + }; + const path = join(evidence, `${suite}.json`); + writeFileSync(path, `${JSON.stringify(receipt, null, 2)}\n`); + log(`${suite}: ${receipt.state}; receipt ${path}`); + return failed ? 1 : 0; +} + +if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) { + process.exitCode = main(process.argv.slice(2)); +} diff --git a/apps/morphir/scripts/extism-python-api.py b/apps/morphir/scripts/extism-python-api.py deleted file mode 100644 index 5b7d9c201..000000000 --- a/apps/morphir/scripts/extism-python-api.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Independent Python API and generated-consumer checks for the CLI fixture.""" -from __future__ import annotations - -import json -import sys -import hashlib -import importlib -from importlib.metadata import version as package_version -from pathlib import Path - -from morphir.ir.v3 import codec as v3_codec -from morphir.ir.v4 import codec as v4_codec -from morphir_tools.python import CompileFailure, IrVersion, SourceDocument, compile_sources, native_compiler - -source, compiled, generated, version = (Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3]), sys.argv[4]) -info = native_compiler() -assert info.compiler_version == sys.argv[5], info -result = compile_sources( - [SourceDocument(p.relative_to(source).as_posix(), p.read_text()) for p in sorted(source.rglob("*.py"))], - package="acme/example", ir_version=IrVersion.V3 if version == "3" else IrVersion.V4, -) -if isinstance(result, CompileFailure): - raise AssertionError(result.diagnostics) -codec = v3_codec if version == "3" else v4_codec -assert codec.decode_versioned_distribution(compiled.read_text()) == result.distribution -for file in generated.rglob("*.py"): - compile(file.read_text(), str(file), "exec") -sys.path.insert(0, str(generated)) -# Generated module paths follow the source package/module names. Find the module -# defining the concrete fixture functions without executing arbitrary test input. -rule_files = [p for p in generated.rglob("*.py") if "def choose(" in p.read_text()] -assert len(rule_files) == 1, rule_files -name = ".".join(rule_files[0].relative_to(generated).with_suffix("").parts) -rules = importlib.import_module(name) -assert rules.choose(True, 5, 7) == 5 -assert rules.choose(False, 5, 7) == 7 -assert rules.twice_identity(9) == 9 -assert rules.large_value() == 9007199254740993 -assert rules.pair(1.0, 2.0) == (1.0, 2.0) -native_module = importlib.import_module("morphir_python_native._native") -native_path = Path(native_module.__file__) -print(json.dumps({"python": sys.version, "nativeCompiler": str(info), "nativeModuleSha256": hashlib.sha256(native_path.read_bytes()).hexdigest(), "packages": {name: package_version(name) for name in ("morphir", "morphir-python", "morphir-python-native")}, "apiParity": True, "consumer": True})) diff --git a/apps/morphir/scripts/test-extism-python.ts b/apps/morphir/scripts/test-extism-python.ts deleted file mode 100644 index 76123dd4a..000000000 --- a/apps/morphir/scripts/test-extism-python.ts +++ /dev/null @@ -1,86 +0,0 @@ -import assert from 'node:assert/strict'; -import {createHash} from 'node:crypto'; -import {spawnSync} from 'node:child_process'; -import {cpSync,existsSync,mkdirSync,mkdtempSync,readFileSync,readdirSync,realpathSync,rmSync,statSync,writeFileSync} from 'node:fs'; -import {tmpdir} from 'node:os'; -import {dirname,join,resolve} from 'node:path'; -import {fileURLToPath} from 'node:url'; - -const repository=fileURLToPath(new URL('../../../',import.meta.url)); -const bundle=realpathSync(process.env.MORPHIR_EXTISM_BUNDLE??join(repository,'.dev/extism/python-bundle')); -const hostPinPath=process.env.MORPHIR_EXTISM_HOST_PIN??join(repository,'.dev/native-services/host-pin.json'); -const hostPin=JSON.parse(readFileSync(hostPinPath,'utf8')); -const hostBuild=JSON.parse(readFileSync(join(dirname(hostPinPath),'evidence.json'),'utf8')); -assert.deepEqual(hostBuild.host,hostPin,'host build receipt must bind the invoked executable'); -const python=process.env.MORPHIR_EXTISM_PYTHON; -assert.ok(python,'MORPHIR_EXTISM_PYTHON must name an independently installed matching morphir-tools Python API'); -const runtime=realpathSync(process.execPath),sha=(p:string)=>createHash('sha256').update(readFileSync(p)).digest('hex'); -const root=realpathSync(mkdtempSync(join(tmpdir(),'morphir-extism-python-'))); -const source=join(repository,'apps/morphir/fixtures/extism-python/v1'); -const descriptor=JSON.parse(readFileSync(join(bundle,'release.json'),'utf8')); -const wasm=descriptor.artifacts.find((a:{runtime:string})=>a.runtime==='wasm'); -assert.equal(sha(join(bundle,wasm.filename)),wasm.sha256); -assert.equal(wasm.claims?.capabilities?.frontend?.multiDocument,true,'full qualification requires an explicitly multi-document Python bundle'); -const native=resolve(process.env.MORPHIR_EXTISM_NATIVE??join(repository,'_build/native/debug/build/morphir/morphir/morphir.exe')); -const release=resolve(process.env.MORPHIR_EXTISM_RELEASE??join(repository,'_build/native/release/build/morphir/morphir/morphir.exe')); -const helpers=join(repository,'apps/morphir/build-provider'); -const required=['extism.mjs','supervision.mjs','windows-job.mjs']; -const results:unknown[]=[]; -const durations:number[]=[]; -function run(program:string,args:string[],status=0,cwd=root){ - const started=performance.now(); - const child=spawnSync(program,args,{cwd,env:{...process.env,NODE_DISABLE_COMPILE_CACHE:'1',MORPHIR_HOME:join(root,'home'),TMPDIR:join(root,'temporary'),HTTP_PROXY:'http://127.0.0.1:9',HTTPS_PROXY:'http://127.0.0.1:9',ALL_PROXY:'http://127.0.0.1:9'},encoding:'utf8',timeout:120000,maxBuffer:20*1024*1024}); - durations.push(performance.now()-started);assert.equal(child.status,status,JSON.stringify(args)+'\n'+child.stdout+'\n'+child.stderr);return child; -} -try{ - mkdirSync(join(root,'temporary'));mkdirSync(join(root,'home'));mkdirSync(join(root,'retained')); - cpSync(bundle,join(root,'retained/python'),{recursive:true}); - // No invocation uses the acquisition directory after the offline snapshot. - const retained=join(root,'retained/python'); - cpSync(source,join(root,'src'),{recursive:true}); - const packed=run('npm',['pack','--ignore-scripts','--pack-destination',root],0,join(repository,'apps/morphir')); - const tarball=join(root,packed.stdout.trim().split('\n').at(-1)!); - mkdirSync(join(root,'npm'));run('tar',['-xzf',tarball,'-C',join(root,'npm')]); - mkdirSync(join(root,'native-package'));cpSync(release,join(root,'native-package/morphir')); - const launchers=[ - {name:'source-native',program:native,prefix:[] as string[],helper:helpers}, - {name:'source-js',program:runtime,prefix:[join(repository,'apps/morphir/bin/morphir.js')],helper:helpers}, - {name:'packaged-native',program:join(root,'native-package/morphir'),prefix:[] as string[],helper:join(root,'npm/package/build-provider')}, - {name:'packaged-npm',program:runtime,prefix:[join(root,'npm/package/bin/morphir.js')],helper:join(root,'npm/package/build-provider')}, - ]; - for(const launcher of launchers){ - const pin={contractVersion:'0.1.0',binding:'extism-wasm',executionProfile:'morphir/extism-mep-0.1/v1',host:hostPin,runtime:{executable:runtime,version:process.version,sha256:sha(runtime)},helper:{root:launcher.helper,modules:Object.fromEntries(required.map(n=>[n,sha(join(launcher.helper,n))]))},artifact:{directory:retained,descriptorSha256:sha(join(retained,'release.json')),wasmSha256:wasm.sha256,extensionId:descriptor.extensionId,version:descriptor.version},origin:'finos/morphir-python release '+descriptor.version}; - const pinFile=join(root,launcher.name+'.json');writeFileSync(pinFile,JSON.stringify(pin)); - const controls=['--extension-pin',pinFile,'--extension-helper',join(launcher.helper,'extism.mjs'),'--json']; - const invoke=(args:string[],status=0)=>{const child=run(launcher.program,[...launcher.prefix,'extension',...args,...controls],status);const report=JSON.parse(child.stdout);assert.equal(report.profile,'morphir/command-result');assert.equal(report.exitCode,status);return {report,stderr:child.stderr};}; - const described=invoke(['describe']).report;assert.equal(described.result.session.extension.id,'morphir-python');assert.equal(described.result.session.provenance,'asserted'); - const help=JSON.parse(run(launcher.program,[...launcher.prefix,'extension','describe','--help','--json']).stdout);assert.ok(help.usage); - run(launcher.program,[...launcher.prefix,'extension','describe','--unknown-extism-option','--json'],2); - const cancel=join(root,launcher.name+'-cancel');writeFileSync(cancel,'');const cancelled=invoke(['describe','--cancel-file',cancel],130).report;assert.equal(cancelled.error.code,'execution.cancelled');assert.ok(!('result' in cancelled)); - const versions=[]; - for(const version of ['3','4']){ - const out=join(root,launcher.name+'-v'+version),generated=out+'-python',again=out+'-again'; - const compiled=invoke(['compile',join(root,'src'),'--package','acme/example','--ir-version',version,'--output',out]).report; - assert.equal(compiled.result.session.negotiated.protocolVersion,'0.1'); - const model=JSON.parse(readFileSync(join(out,'morphir-ir.json'),'utf8'));assert.equal(model.formatVersion,Number(version)); - invoke(['generate',join(out,'morphir-ir.json'),'--ir-version',version,'--output',generated]); - const parity=JSON.parse(run(python!,[join(repository,'apps/morphir/scripts/extism-python-api.py'),join(root,'src'),join(out,'morphir-ir.json'),generated,version,descriptor.version]).stdout); - invoke(['compile',generated,'--package','acme/example','--ir-version',version,'--output',again]); - assert.deepEqual(JSON.parse(readFileSync(join(again,'morphir-ir.json'),'utf8')),model); - const invalid=invoke(['generate',join(out,'morphir-ir.json'),'--ir-version',version==='3'?'4':'3','--output',out+'-mismatch'],1).report; - assert.equal(invalid.state,'failed');assert.ok(!('result' in invalid));assert.equal(existsSync(out+'-mismatch'),false); - versions.push({version,parity,modelSha256:sha(join(out,'morphir-ir.json')),generated:readdirSync(generated)}); - } - for(const args of [ ['compile',join(root,'src'),'--package','acme/example','--output',join(root,'missing')], ['compile',join(root,'src'),'--package','acme/example','--ir-version','3.1.0','--output',join(root,'unsupported')], ['compile',join(root,'src'),'--package','acme/example','--ir-version','3','--model-format','ion','--output',join(root,'ion')] ]) assert.equal(invoke(args,1).report.state,'failed'); - const bad={...pin,artifact:{...pin.artifact,wasmSha256:'0'.repeat(64)}};writeFileSync(pinFile,JSON.stringify(bad));assert.equal(invoke(['describe'],1).report.state,'failed');writeFileSync(pinFile,JSON.stringify(pin)); - writeFileSync(join(root,'broken.py'),'def broken(:\n');const diagnosed=invoke(['compile',join(root,'broken.py'),'--package','acme/example','--ir-version','3','--output',join(root,launcher.name+'-broken')],1);assert.ok(diagnosed.report.diagnostics.some((d:{code?:string})=>d.code?.startsWith('PY'))); - assert.deepEqual(readdirSync(join(root,'temporary')),[],'owned scratch was reaped and cleaned'); - results.push({name:launcher.name,programSha256:sha(launcher.program),entrypoint:launcher.prefix.map(p=>({file:p,sha256:sha(p)})),helpers:pin.helper.modules,versions}); - } - const sourceFiles=['apps/morphir/cli/extism.mbt','apps/morphir/cli/extension.mbt','apps/morphir/cli/arguments.mbt','apps/morphir/native-services/main.rs','apps/morphir/native-services/extism.rs','apps/morphir/native-services/extism_tests.rs','apps/morphir/native-services/Cargo.lock','apps/morphir/dist/morphir.js','apps/morphir/build-provider/extism.mts','apps/morphir/build-provider/test-extism.mts','apps/morphir/fixtures/extism-python/v1/acme/models.py','apps/morphir/fixtures/extism-python/v1/acme/rules.py','apps/morphir/scripts/extism-python-api.py','scripts/extism-python.mbtx']; - const receipt={profile:'morphir/extism-python-qualification',contractVersion:'0.1.0',state:'succeeded',platform:process.platform,architecture:process.arch,node:process.version,runtimeSha256:sha(runtime),python:{executable:python,sha256:sha(python!)},host:hostPin,hostBuild:{sourceRevision:hostBuild.sourceRevision,sharedRustTree:hostBuild.sharedRustTree,target:hostBuild.target,rust:hostBuild.rust,moonbit:hostBuild.moonbit,cargoLockSha256:hostBuild.cargoLockSha256},hostBytes:statSync(hostPin.executable).size,bundle:{descriptorSha256:sha(join(bundle,'release.json')),wasmSha256:wasm.sha256,extensionId:descriptor.extensionId,version:descriptor.version,gitCommit:descriptor.gitCommit},checkDefinitionSha256:sha(fileURLToPath(import.meta.url)),sourceFiles:Object.fromEntries(sourceFiles.map(p=>[p,sha(join(repository,p))])),sourceRevision:spawnSync('git',['rev-parse','HEAD'],{cwd:repository,encoding:'utf8'}).stdout.trim(),elapsedCallsMs:durations,results}; - const reusablePin={contractVersion:'0.1.0',binding:'extism-wasm',executionProfile:'morphir/extism-mep-0.1/v1',host:hostPin,runtime:{executable:runtime,version:process.version,sha256:sha(runtime)},helper:{root:helpers,modules:Object.fromEntries(required.map(n=>[n,sha(join(helpers,n))]))},artifact:{directory:bundle,descriptorSha256:sha(join(bundle,'release.json')),wasmSha256:wasm.sha256,extensionId:descriptor.extensionId,version:descriptor.version},origin:'finos/morphir-python release '+descriptor.version}; - mkdirSync(join(repository,'.dev/extism'),{recursive:true}); - writeFileSync(join(repository,'.dev/extism/python-pin.json'),JSON.stringify(reusablePin,null,2)+'\n'); - const destination=join(repository,'.dev/extism/qualification.json');writeFileSync(destination,JSON.stringify(receipt,null,2)+'\n');console.log(JSON.stringify(receipt)); -}finally{rmSync(root,{recursive:true,force:true});} diff --git a/docs/MISE_TASKS.md b/docs/MISE_TASKS.md index 2c984fe7a..bd2e2e51b 100644 --- a/docs/MISE_TASKS.md +++ b/docs/MISE_TASKS.md @@ -77,7 +77,12 @@ compiler/core pair. | `mise run test:workbench-capability-execution-mck` | Opt-in exact draft.19 immutable Scheme execution traces in Node/Chromium/native debug/release; requires explicit parent runner/kit and Workbench Chromium dependencies. Actual isolated-worker acceptance is separate | All | | `mise run test:conformance` | Compare versioned independent Ion expectations, live Scheme and explicitly pinned optional Rust; retain separate coverage receipts | All | | `mise run test:cli` | Run CLI target and npm package smoke tests | All | -| `mise run test:cli-extism-python -- --shared /absolute/morphir --bundle /absolute/python-bundle --python /absolute/venv/bin/python --json` | Opt-in pinned Python Extism IR 3/4 qualification through source and packaged CLIs; requires an existing matching Python API and bundle. It still writes a retired pin and is rewritten for the worker host in group 7 of `host-extensions-as-workers`; see [qualification](../apps/morphir/docs/extension.md#qualification) | Waits for the WASM worker host | +| `mise run test:cli-extension-elm -- --json` | Qualify the pinned Elm extension executables (`v0.4.1`, elm-classic `v0.5.1`) and the Elm `v0.5.0-preview.2` WASM module on the worker host from the native and npm CLIs: describe, compile of the Elm fixtures, generate (WASM), and no Node on the native run's PATH. See [qualification](../apps/morphir/docs/extension.md#qualification) | macOS, Linux; manual | +| `mise run test:cli-extism-python -- --json` | Qualify the pinned Python extension `0.4.1` (one document, multi-document refusal) and `0.4.2` (IR 3 and 4 compile, generate, consumer, recompile) from the native and npm CLIs; `--python PATH` names Python 3.12 or later for the consumer | macOS, Linux; manual | +| `mise run test:cli-native-prerequisites -- --json` | Qualify the native MoonBit guest as a process worker from both CLIs with the shared kit's source, consumer and JSON artifacts, and run the shared MEP kit against it | macOS, Linux; manual | +| `mise run test:portable-mep-cli -- --json` | Qualify the portable MoonBit guest (`morphir-bytes-v1`) from both CLIs, and run the shared MEP kit against it through the native CLI's worker mode | macOS, Linux; manual | +| `mise run test:extension-conformance -- --json` | Run the `morphir-extension-sdk` fixture guest in `apps/morphir/fixtures/sdk-guest` from both CLIs: imports, claims, compile, generate and host functions | macOS, Linux; manual | +| `mise run test:extension-qualification -- --json` | Run all five suites above and write one receipt for each | macOS, Linux; manual | | `mise run test:moonbit-frontend` | Compare original Boolean MoonBit, Scheme and generated libraries across four targets in debug and release; requires pinned `MOON_HOME` and retains identities | All | | `mise run test:supervision` | Verify process-tree cleanup, cancellation, deadlines and bounded diagnostics on Linux, macOS and Windows | All | | `mise run test:embedding` | Install embedding dependencies and Chromium, then test Node, installed npm and browser hosts | All | diff --git a/openspec/changes/host-extensions-as-workers/tasks.md b/openspec/changes/host-extensions-as-workers/tasks.md index 444499530..5cc1e6d0c 100644 --- a/openspec/changes/host-extensions-as-workers/tasks.md +++ b/openspec/changes/host-extensions-as-workers/tasks.md @@ -113,20 +113,31 @@ ## 7. Qualify the published extensions on the worker host (morphir-ukvn.5.64.36.7) -- [ ] 7.1 Qualify the Elm extension `v0.4.1` executables from the native and npm CLI on macOS arm64 and Linux x64; - verify compile of the Elm fixture and the absence of Node from the native run's environment -- [ ] 7.2 Rewrite `test:cli-extism-python` for `0.2.0` pins and qualify Python `0.4.1` (single document and the +- [x] 7.1 Qualify the Elm extension `v0.4.1` executables from the native and npm CLI on macOS arm64 and Linux x64; + verify compile of the Elm fixture and the absence of Node from the native run's environment. Describe and the + no-Node check pass; `v0.4.1` compile is a recorded failure of the guest, which requires `package.exposedModules` + that MEP makes optional (bead `morphir-ukvn.5.64.36.16`). Its fix, elm-classic `v0.5.1`, compiles the fixture from + both CLIs, and the Elm `v0.5.0-preview.2` WASM module is qualified as a second Elm guest; no host change +- [x] 7.2 Rewrite `test:cli-extism-python` for `0.2.0` pins and qualify Python `0.4.1` (single document and the multi-document refusal) and `0.4.2` (IR 3 and 4 compile and generate, consumer check) from both CLIs; verify the receipts -- [ ] 7.3 Rewrite `test:portable-mep-cli` and `test:cli-native-prerequisites` for the worker host and qualify the - MoonBit native and portable guests with the shared MCK kit; verify the kit's expected output -- [ ] 7.4 Add a conformance fixture guest built with `morphir-extension-sdk` at a pinned revision, importing WASI and +- [x] 7.3 Rewrite `test:portable-mep-cli` and `test:cli-native-prerequisites` for the worker host and qualify the + MoonBit native and portable guests with the shared MCK kit; verify the kit's expected output. The kit's source, + consumer and JSON artifacts pass from both CLIs; the shared kit run fails at case `compile-ion` because the kit + asks for the retired profile `moonbit-model-bool-library-v1` (follow-up bead `morphir-ukvn.5.64.36.17`, + finos/morphir) +- [x] 7.4 Add a conformance fixture guest built with `morphir-extension-sdk` at a pinned revision, importing WASI and the Morphir host functions, stored with its source and build receipt under `apps/morphir/fixtures`; run the shared MEP conformance kit against it and against the MoonBit guest from both CLIs; verify the "Conformance, not a list of - extensions" scenarios with no host change made for that guest + extensions" scenarios with no host change made for that guest. No shared kit profile fits the fixture guest, so + it passes a local conformance check (`test:extension-conformance`); the shared-kit rerun is pending bead + `morphir-ukvn.5.64.36.15` - [ ] 7.5 Run one Windows x64 qualification of tasks 7.1 to 7.4 after a build-master review; verify the receipts and - add a `Tested-on:` trailer -- [ ] 7.6 Record every receipt with its exact artifact digests as a comment on this bead; verify each receipt names + add a `Tested-on:` trailer. Run 41241517957 failed: every guest session runs, but no output is published, because the + native `_WIN32` directory rename is a stub and the Node publication creates the destination before it renames onto + it (bead `morphir-ukvn.5.64.36.19`); the portable guest is not packaged on Windows (bead `morphir-ukvn.5.64.36.18`). + No `Tested-on:` trailer until a rerun passes +- [x] 7.6 Record every receipt with its exact artifact digests as a comment on this bead; verify each receipt names its platform, CLI build and guest digest ## 8. Browser Web Worker feasibility (morphir-ukvn.5.64.36.8) diff --git a/scripts/extension-qualification.mbtx b/scripts/extension-qualification.mbtx new file mode 100644 index 000000000..029196635 --- /dev/null +++ b/scripts/extension-qualification.mbtx @@ -0,0 +1,533 @@ +---- +import { + "moonbitlang/core/argparse", + "moonbitlang/core/env", + "moonbitlang/core/json", + "moonbitlang/async@0.22.7", + "moonbitlang/async@0.22.7/fs", + "moonbitlang/async@0.22.7/process", + "moonbitlang/async@0.22.7/stdio", + "moonbitlang/x@0.5.5/crypto", + "moonbitlang/x@0.5.5/sys", +} +---- + +// Qualification of the published extensions and the conformance fixture guest +// on the CLI's worker host (group 7 of the OpenSpec change +// host-extensions-as-workers). It builds the native CLI and the npm package, +// and the MoonBit guests when a suite needs them, then runs +// apps/morphir/scripts/extension-qualification.ts once for each suite. That +// driver downloads the pinned artifacts into new directories, checks their +// SHA-256, runs every operation from the native and the npm CLI and writes one +// receipt for each suite. This script is manual: no PR or merge-queue job runs +// it. + +///| +/// One qualification suite of the driver. +enum Suite { + Elm + Python + MoonbitNative + MoonbitPortable + Conformance +} derive(Eq) + +///| +let all_suites : Array[Suite] = [ + Elm, + Python, + MoonbitNative, + MoonbitPortable, + Conformance, +] + +///| +/// The suites that a SUITE argument selects: one suite, or every suite for +/// `all`. +fn Suite::select(text : String) -> Array[Suite]? { + match text { + "all" => Some(all_suites) + "elm" => Some([Elm]) + "python" => Some([Python]) + "moonbit-native" => Some([MoonbitNative]) + "moonbit-portable" => Some([MoonbitPortable]) + "conformance" => Some([Conformance]) + _ => None + } +} + +///| +fn Suite::name(self : Suite) -> String { + match self { + Elm => "elm" + Python => "python" + MoonbitNative => "moonbit-native" + MoonbitPortable => "moonbit-portable" + Conformance => "conformance" + } +} + +///| +let about : String = + #|Qualify the published extensions and the SDK conformance fixture guest on the CLI's worker host, from the native CLI and the npm CLI (an unpacked npm pack of apps/morphir). SUITE is elm (Elm extension executables v0.4.1 and elm-classic v0.5.1, and the Elm v0.5.0-preview.2 WASM module), python (Python extension 0.4.1 and 0.4.2), moonbit-native and moonbit-portable (the MoonBit guest as a process worker or a morphir-bytes-v1 WASM worker, with the shared MEP kit), conformance (the morphir-extension-sdk fixture guest in apps/morphir/fixtures/sdk-guest) or all. + #| + #|The script builds the inputs (moon build of apps/morphir and apps/morphir-extension, bun run build, npm pack, and scripts/package-mep-extension.mbtx for moonbit-portable), then runs apps/morphir/scripts/extension-qualification.ts for each suite. The driver downloads each pinned artifact of apps/morphir/fixtures/qualification/published.json into a new, empty directory and uses it only when its SHA-256 equals the pin. It writes /.json, one receipt with one row for each operation: extension, version, cli (native, npm or mck), platform, operation, result (passed, failed or known-failure, with the finding and its follow-up bead), guest SHA-256 and output digests. A known failure is an operation that fails for a recorded cause outside the host; it does not fail the suite, and it fails the suite when it passes. + #| + #|Build, download and driver output goes to stderr. Without --json, stdout has one summary line for each suite. With --json, stdout is one morphir/command-result 0.1.0 value written after every receipt: result {evidence, suites: [{suite, state, receiptPath, receiptSha256, passed, knownFailures, failed}], receipts: []}. A suite whose inputs could not be built (for example the portable guest's packaging) has state failed and error instead of a receipt, and the other suites still run. A failure has error {code, message} instead of result: qualification_failed (exit 1) when a suite failed, step_failed with the exit status of a failed build step, invalid_arguments (exit 2) for invalid arguments. --help prints this help and runs nothing; --help --json returns it as result.help. + +///| +fn command() -> @argparse.Command { + @argparse.Command( + "extension-qualification", + about~, + disable_help_flag=true, + disable_help_subcommand=true, + positionals=[ + @argparse.PositionArg( + "SUITE", + about="elm, python, moonbit-native, moonbit-portable, conformance or all", + ), + ], + flags=[ + @argparse.FlagArg("json", about="Write one command-result on stdout"), + @argparse.FlagArg("help", short='h', about="Print the help; run nothing"), + ], + options=[ + @argparse.OptionArg( + "evidence", + about="Directory for receipts and scratch files; default .dev/extension-qualification/evidence", + ), + @argparse.OptionArg( + "python", + about="Python 3.12 or later for the Python consumer check; default: uv python find '>=3.12'", + ), + ], + ) +} + +///| +suberror StepFailed { + StepFailed(Int32, String) +} + +///| +/// Runs one build step with its output on stderr. +async fn step( + program : String, + args : Array[String], + env? : Map[String, String], +) -> Unit { + let code = @process.run( + program, + args, + extra_env=env.unwrap_or(Map([])), + stdout=@stdio.stderr, + stderr=@stdio.stderr, + ) + guard code == 0 else { + raise StepFailed( + if code > 0 { + code + } else { + 1 + }, + "\{program} \{args.join(" ")} failed with status \{code}", + ) + } +} + +///| +async fn sha256_of(path : String) -> String { + @crypto.bytes_to_hex_string(@crypto.sha256(@fs.read_file(path).binary())) +} + +///| +fn field(value : Json, name : String) -> Json { + match value { + Object(fields) => fields.get(name).unwrap_or(Json::null()) + _ => Json::null() + } +} + +///| +/// The number of receipt rows with a result. +fn count(receipt : Json, result : String) -> Int32 { + match field(receipt, "rows") { + Array(rows) => + rows.filter(row => field(row, "result") == Json::string(result)).length() + _ => 0 + } +} + +///| +/// The summary of one receipt. +fn summary( + suite : Suite, + path : String, + digest : String, + receipt : Json, +) -> Json { + { + "suite": suite.name(), + "state": field(receipt, "state"), + "receiptPath": path, + "receiptSha256": digest, + "passed": count(receipt, "passed"), + "knownFailures": count(receipt, "known-failure"), + "failed": count(receipt, "failed"), + } +} + +///| +/// The npm command-line script that ships with the Node on PATH: +/// /node_modules/npm on Windows, /lib/node_modules/npm +/// elsewhere. +async fn npm_cli() -> String { + let (code, out, err) = @process.collect_output("node", ["-e", npm_cli_probe]) + let path = out.text().trim().to_owned() + guard code == 0 && path != "" else { + raise StepFailed( + 1, + "cannot find npm beside node: " + err.text().trim().to_owned(), + ) + } + path +} + +///| +let npm_cli_probe : String = + #|const {dirname, join} = require('node:path'); const {existsSync} = require('node:fs'); + #|const prefix = dirname(process.execPath); + #|const found = [join(prefix, 'node_modules/npm/bin/npm-cli.js'), join(prefix, '../lib/node_modules/npm/bin/npm-cli.js')].find(existsSync); + #|if (!found) process.exit(1); console.log(found); + +///| +/// Packages the portable MoonBit guest into `portable`, afresh. +async fn package_portable(portable : String) -> Unit { + step("bun", [ + "install", "--frozen-lockfile", "--cwd", "pkgs/morphir-host/embedding", + ]) + // The packaging refuses an existing destination. + if @fs.exists(portable) { + @fs.rmdir(portable, recursive=true) + } + step("moonx", ["scripts/package-mep-extension.mbtx"], env={ + "MORPHIR_MEP_ARTIFACTS": portable, + }) +} + +///| +/// The summary of a suite whose inputs could not be built: failed, with no +/// receipt. +fn unbuilt(suite : Suite, message : String) -> Json { + { + "suite": suite.name(), + "state": "failed", + "error": message, + "passed": 0, + "knownFailures": 0, + "failed": 0, + } +} + +///| +/// Python 3.12 or later for the consumer check, from uv when it is installed. +async fn find_python() -> String? { + let (code, out, _) = @process.collect_output("uv", [ + "python", "find", ">=3.12", + ]) catch { + _ => return None + } + let path = out.text().trim().to_owned() + if code == 0 && path != "" { + Some(path) + } else { + None + } +} + +///| +async fn execute( + suites : Array[Suite], + evidence : String, + python : String?, +) -> (Bool, Json) { + let root = @fs.realpath(".") + // The directory may exist: the workflow's step helper makes it first. + if !@fs.exists(evidence) { + @fs.mkdir(evidence, recursive=true) + } + let evidence = @fs.realpath(evidence) + let built = "\{root}/_build/native/release/build" + step("moon", ["build", "--target", "native", "--release", "apps/morphir"]) + step("bun", ["install", "--frozen-lockfile"]) + step("bun", ["run", "--cwd", "apps/morphir", "build"]) + // The npm CLI is the package that npm pack makes, unpacked in a new + // directory, as a user installs it. + let npm = "\{evidence}/npm" + if @fs.exists(npm) { + @fs.rmdir(npm, recursive=true) + } + @fs.mkdir(npm, recursive=true) + // npm runs through Node: on Windows npm is npm.cmd, which a process call + // that adds .exe cannot start. + step("node", [ + npm_cli(), + "pack", + "--ignore-scripts", + "--silent", + "--pack-destination", + npm, + "./apps/morphir", + ]) + step("tar", ["-xzf", "\{npm}/morphir-morphir-0.1.0.tgz", "-C", npm]) + let moonbit = suites.contains(MoonbitNative) || + suites.contains(MoonbitPortable) + if moonbit { + step("moon", [ + "build", "--target", "native", "--release", "apps/morphir-extension", + ]) + } + let portable = "\{evidence}/portable-mep" + // A packaging failure fails only the moonbit-portable suite; the other + // suites still run. + let portable_error : Ref[String?] = Ref(None) + if suites.contains(MoonbitPortable) { + package_portable(portable) catch { + StepFailed(_, message) => portable_error.val = Some(message) + error => portable_error.val = Some(error.to_string()) + } + } + let python = match python { + Some(path) => Some(path) + None => if suites.contains(Python) { find_python() } else { None } + } + let summaries : Array[Json] = [] + let receipts : Array[Json] = [] + let mut passed = true + for suite in suites { + if suite == MoonbitPortable && portable_error.val is Some(message) { + passed = false + summaries.push( + unbuilt(suite, "the portable guest was not packaged: " + message), + ) + continue + } + let receipt_path = "\{evidence}/\{suite.name()}.json" + if @fs.exists(receipt_path) { + @fs.remove(receipt_path) + } + let args = [ + "apps/morphir/scripts/extension-qualification.ts", + "--suite", + suite.name(), + "--evidence", + evidence, + "--native", + "\{built}/morphir/morphir/morphir.exe", + "--npm-entry", + "\{npm}/package/bin/morphir.js", + "--native-guest", + "\{built}/finos/morphir-extension/morphir-extension.exe", + "--portable", + "\{portable}/extension", + ] + if python is Some(path) { + args.append(["--python", path]) + } + let code = @process.run( + "node", + args, + stdout=@stdio.stderr, + stderr=@stdio.stderr, + ) + guard @fs.exists(receipt_path) else { + raise StepFailed( + if code > 0 { + code + } else { + 1 + }, + "the driver wrote no receipt for \{suite.name()} (status \{code})", + ) + } + let receipt = @fs.read_file(receipt_path).json() + let item = summary(suite, receipt_path, sha256_of(receipt_path), receipt) + if code != 0 || field(receipt, "state") != Json::string("passed") { + passed = false + } + summaries.push(item) + receipts.push(receipt) + } + ( + passed, + { + "evidence": evidence, + "suites": Json::array(summaries), + "receipts": Json::array(receipts), + }, + ) +} + +///| +fn document(state : String, code : Int32, body : (String, Json)) -> String { + let fields : Map[String, Json] = { + "profile": "morphir/command-result", + "contractVersion": "0.1.0", + "command": "extension-qualification", + "state": state, + "exitCode": code, + "diagnostics": [], + } + fields[body.0] = body.1 + Json::object(fields).stringify() + "\n" +} + +///| +async fn refuse( + json : Bool, + code : Int32, + kind : String, + message : String, +) -> Unit { + if json { + @stdio.stdout.write( + document("failed", code, ("error", { "code": kind, "message": message })), + ) + } else { + @stdio.stderr.write("extension-qualification: \{message}\n") + } + @sys.exit(code) +} + +///| +fn summary_line(item : Json) -> String { + let text = fn(name : String) { + match field(item, name) { + String(value) => value + Number(value, ..) => value.to_int().to_string() + _ => "" + } + } + let line = "\{text("suite")}: \{text("state")} (\{text("passed")} passed, \{text("knownFailures")} known failures, \{text("failed")} failed) \{text("receiptPath")}" + match field(item, "error") { + String(error) => line + ": " + error + _ => line + } +} + +///| +async fn main { + let json = @env.args().contains("--json") + let matches = command().parse(argv=@env.args()[1:]) catch { + error => { + refuse(json, 2, "invalid_arguments", error.to_string()) + return + } + } + let json = matches.flags.get("json").unwrap_or(false) + if matches.flags.get("help").unwrap_or(false) { + if json { + @stdio.stdout.write( + document( + "succeeded", + 0, + ("result", { "help": command().render_help() }), + ), + ) + } else { + @stdio.stdout.write(command().render_help() + "\n") + } + return + } + let suites = match matches.values.get("SUITE") { + Some([text, ..]) => Suite::select(text) + _ => None + } + guard suites is Some(suites) else { + refuse( + json, 2, "invalid_arguments", "SUITE must be elm, python, moonbit-native, moonbit-portable, conformance or all", + ) + return + } + let evidence = match matches.values.get("evidence") { + Some([path, ..]) => path + _ => ".dev/extension-qualification/evidence" + } + let python = match matches.values.get("python") { + Some([path, ..]) => Some(path) + _ => None + } + let (passed, result) = execute(suites, evidence, python) catch { + StepFailed(code, message) => { + refuse(json, code, "step_failed", message) + return + } + error => { + refuse(json, 1, "command_failed", error.to_string()) + return + } + } + guard field(result, "suites") is Array(items) else { return } + if !passed { + let failed = items + .filter(item => field(item, "state") != Json::string("passed")) + .map(summary_line) + refuse( + json, + 1, + "qualification_failed", + "a suite failed: " + failed.join("; "), + ) + return + } + if json { + @stdio.stdout.write(document("succeeded", 0, ("result", result))) + } else { + for item in items { + @stdio.stdout.write(summary_line(item) + "\n") + } + } +} + +///| +test "a SUITE argument selects suites" { + assert_eq(Suite::select("all").map(suites => suites.length()), Some(5)) + assert_true(Suite::select("python") is Some([Python])) + assert_true(Suite::select("moonbit-portable") is Some([MoonbitPortable])) + assert_true(Suite::select("rust") is None) + assert_eq(all_suites.map(Suite::name), [ + "elm", "python", "moonbit-native", "moonbit-portable", "conformance", + ]) +} + +///| +test "a summary counts the rows of each result" { + let receipt : Json = { + "state": "passed", + "rows": [ + { "result": "passed" }, + { "result": "passed" }, + { "result": "known-failure" }, + ], + } + let item = summary(Elm, "/e/elm.json", "ab", receipt) + assert_eq(field(item, "passed"), Json::number(2.0)) + assert_eq(field(item, "knownFailures"), Json::number(1.0)) + assert_eq(field(item, "failed"), Json::number(0.0)) + assert_eq( + summary_line(item), + "elm: passed (2 passed, 1 known failures, 0 failed) /e/elm.json", + ) +} + +///| +test "a suite whose inputs were not built is failed with its cause" { + let item = unbuilt(MoonbitPortable, "the portable guest was not packaged: x") + assert_eq(field(item, "state"), Json::string("failed")) + assert_eq(field(item, "suite"), Json::string("moonbit-portable")) + assert_eq( + field(item, "error"), + Json::string("the portable guest was not packaged: x"), + ) +} diff --git a/scripts/extism-python.mbtx b/scripts/extism-python.mbtx deleted file mode 100644 index 3b18a7225..000000000 --- a/scripts/extism-python.mbtx +++ /dev/null @@ -1,711 +0,0 @@ ----- -import { - "moonbitlang/core/argparse", - "moonbitlang/core/env", - "moonbitlang/core/json", - "moonbitlang/async@0.22.7", - "moonbitlang/async@0.22.7/fs", - "moonbitlang/async@0.22.7/process", - "moonbitlang/async@0.22.7/stdio", - "moonbitlang/x@0.5.5/crypto", - "moonbitlang/x@0.5.5/sys", -} ----- - -///| -/// Opt-in qualification of an existing Python Extism bundle. Acquisition and -/// dependency installation are explicit prerequisites, never side effects. -suberror CommandFailure { - CommandFailure(Int32, String, String) -} - -///| -async fn checked( - program : String, - args : Array[String], - environment~ : Map[String, String] = Map([]), -) -> Unit { - let moon_home = @env.get_env_var("MOON_HOME").unwrap_or("") - let executable = if program == "moon" && moon_home != "" { - moon_home + "/bin/moon" - } else { - program - } - let variables = environment.copy() - if moon_home != "" { - variables["PATH"] = moon_home + - "/bin:" + - @env.get_env_var("PATH").unwrap_or("") - } - let status = @process.run( - executable, - args, - extra_env=variables, - stdout=@stdio.stderr, - stderr=@stdio.stderr, - ) - guard status == 0 else { - raise CommandFailure( - if status > 0 { - status - } else { - 1 - }, - "extism.qualification_failed", - program + " failed with status " + status.to_string(), - ) - } -} - -///| -fn required(matches : @argparse.Matches, key : String) -> String raise { - let value = match matches.values.get(key) { - Some([value]) => value - _ => "" - } - guard value.has_prefix("/") && !value.contains("\u0000") else { - raise CommandFailure( - 1, - "extism.invalid_input", - "--" + key + " must name an explicit absolute local path", - ) - } - value -} - -///| -fn field(value : Json, key : String) -> Json { - match value { - Object(fields) => fields.get(key).unwrap_or(Json::null()) - _ => Json::null() - } -} - -///| -async fn digest(path : String) -> String { - @crypto.bytes_to_hex_string(@crypto.sha256(@fs.read_file(path).binary())) -} - -///| -async fn capture(program : String, args : Array[String]) -> String { - let (status, output, errors) = @process.collect_output(program, args) - if errors.text() != "" { - @stdio.stderr.write(errors.text()) - } - guard status == 0 else { - raise CommandFailure( - if status > 0 { - status - } else { - 1 - }, - "extism.qualification_failed", - "Unable to inspect " + program + " for candidate-bound qualification", - ) - } - output.text() -} - -///| -/// The caller takes this snapshot before the real-bundle suite and compares it -/// again afterwards. A source/artifact change while tests run cannot be blessed -/// by a receipt that merely hashes the new bytes after the fact. -async fn candidate(root : String, bundle : String, python : String) -> Json { - let node = @json.parse( - capture("node", [ - "-p", "JSON.stringify({platform:process.platform,architecture:process.arch,node:process.version,executable:process.execPath})", - ]), - ) - guard field(node, "executable") is String(runtime) else { - raise CommandFailure( - 1, "extism.invalid_receipt", "Node runtime identity is missing", - ) - } - let host = @json.parse( - @fs.read_file(root + "/.dev/native-services/host-pin.json").text(), - ) - guard field(host, "executable") is String(host_program) && - field(host, "sha256") == Json::string(digest(host_program)) else { - raise CommandFailure( - 1, "extism.invalid_receipt", "Pinned host bytes changed", - ) - } - let host_evidence = @json.parse( - @fs.read_file(root + "/.dev/native-services/evidence.json").text(), - ) - guard field(host_evidence, "host") == host && - field(host_evidence, "sourceRevision") == field(host, "sourceRevision") && - field(host_evidence, "cargoLockSha256") == - Json::string(digest(root + "/.dev/native-services/Cargo.lock")) else { - raise CommandFailure( - 1, "extism.invalid_receipt", "Host build evidence does not match its executable or lock", - ) - } - let host_build : Map[String, Json] = Map([]) - for - key in [ - "sourceRevision", "sharedRustTree", "target", "rust", "moonbit", "cargoLockSha256", - ] { - guard field(host_evidence, key) is String(text) && text != "" else { - raise CommandFailure( - 1, - "extism.invalid_receipt", - "Missing host build identity: " + key, - ) - } - host_build[key] = Json::string(text) - } - let descriptor = @json.parse(@fs.read_file(bundle + "/release.json").text()) - guard field(descriptor, "artifacts") is Array(artifacts) && - artifacts - .iter() - .find_first(a => field(a, "runtime") == Json::string("wasm")) - is Some(wasm) && - field(descriptor, "version") is String(version) && - field(wasm, "filename") is String(filename) && - filename != "" && - !filename.has_prefix("/") && - !filename.contains("\\") && - filename.split("/").all(part => part != "" && part != "." && part != "..") && - field(wasm, "sha256") == Json::string(digest(bundle + "/" + filename)) else { - raise CommandFailure( - 1, "extism.invalid_receipt", "Python WASM bytes changed", - ) - } - let descriptor_sha = digest(bundle + "/release.json") - let bundle_fields : Map[String, Json] = Map::from_array([ - ("descriptorSha256", Json::string(descriptor_sha)), - ("wasmSha256", field(wasm, "sha256")), - ("extensionId", field(descriptor, "extensionId")), - ("version", field(descriptor, "version")), - ]) - if descriptor is Object(fields) && fields.get("gitCommit") is Some(commit) { - bundle_fields["gitCommit"] = commit - } - let source_files : Map[String, Json] = Map([]) - for - path in [ - "apps/morphir/cli/extism.mbt", "apps/morphir/cli/extension.mbt", "apps/morphir/cli/arguments.mbt", - "apps/morphir/native-services/main.rs", "apps/morphir/native-services/extism.rs", - "apps/morphir/native-services/extism_tests.rs", "apps/morphir/native-services/Cargo.lock", - "apps/morphir/dist/morphir.js", "apps/morphir/build-provider/extism.mts", "apps/morphir/build-provider/test-extism.mts", - "apps/morphir/fixtures/extism-python/v1/acme/models.py", "apps/morphir/fixtures/extism-python/v1/acme/rules.py", - "apps/morphir/scripts/extism-python-api.py", "scripts/extism-python.mbtx", - ] { - source_files[path] = Json::string(digest(root + "/" + path)) - } - let modules : Map[String, Json] = Map([]) - for name in ["extism.mjs", "supervision.mjs", "windows-job.mjs"] { - modules[name] = Json::string( - digest(root + "/apps/morphir/build-provider/" + name), - ) - } - let runtime_sha = digest(runtime) - let entry_sha = digest(root + "/apps/morphir/bin/morphir.js") - let launchers : Json = { - "source-native": { - "programSha256": digest( - root + "/_build/native/debug/build/morphir/morphir/morphir.exe", - ), - "entrypointSha256": Json::null(), - }, - "source-js": { "programSha256": runtime_sha, "entrypointSha256": entry_sha }, - "packaged-native": { - "programSha256": digest( - root + "/_build/native/release/build/morphir/morphir/morphir.exe", - ), - "entrypointSha256": Json::null(), - }, - "packaged-npm": { - "programSha256": runtime_sha, - "entrypointSha256": entry_sha, - }, - } - let expected_pin : Json = { - "contractVersion": "0.1.0", - "binding": "extism-wasm", - "executionProfile": "morphir/extism-mep-0.1/v1", - "host": host, - "runtime": { - "executable": runtime, - "version": field(node, "node"), - "sha256": runtime_sha, - }, - "helper": { - "root": root + "/apps/morphir/build-provider", - "modules": Json::object(modules), - }, - "artifact": { - "directory": bundle, - "descriptorSha256": descriptor_sha, - "wasmSha256": field(wasm, "sha256"), - "extensionId": field(descriptor, "extensionId"), - "version": field(descriptor, "version"), - }, - "origin": "finos/morphir-python release " + version, - } - { - "platform": field(node, "platform"), - "architecture": field(node, "architecture"), - "node": field(node, "node"), - "runtimeSha256": runtime_sha, - "python": { "executable": python, "sha256": digest(python) }, - "host": host, - "hostBuild": Json::object(host_build), - "bundle": Json::object(bundle_fields), - "sourceRevision": capture("git", ["rev-parse", "HEAD"]).trim().to_owned(), - "sourceFiles": Json::object(source_files), - "checkDefinitionSha256": digest( - root + "/apps/morphir/scripts/test-extism-python.ts", - ), - "helpers": Json::object(modules), - "launchers": launchers, - "pin": expected_pin, - } -} - -///| -fn receipt_valid(receipt : Json, expected : Json) -> Bool { - if field(receipt, "profile") != - Json::string("morphir/extism-python-qualification") || - field(receipt, "contractVersion") != Json::string("0.1.0") || - field(receipt, "state") != Json::string("succeeded") { - return false - } - for - key in [ - "platform", "architecture", "node", "runtimeSha256", "python", "host", "hostBuild", - "bundle", "sourceRevision", "sourceFiles", "checkDefinitionSha256", - ] { - if field(receipt, key) != field(expected, key) { - return false - } - } - guard field(receipt, "results") is Array(results) && results.length() == 4 else { - return false - } - let seen : Map[String, Bool] = Map([]) - for result in results { - guard field(result, "name") is String(name) && - ["source-native", "source-js", "packaged-native", "packaged-npm"].contains( - name, - ) && - !seen.contains(name) else { - return false - } - seen[name] = true - let launcher = field(field(expected, "launchers"), name) - if field(result, "programSha256") != field(launcher, "programSha256") || - field(result, "helpers") != field(expected, "helpers") { - return false - } - guard field(result, "entrypoint") is Array(entries) else { return false } - match field(launcher, "entrypointSha256") { - Null => if !entries.is_empty() { return false } - String(hash) => - if entries.length() != 1 || - field(entries[0], "sha256") != Json::string(hash) { - return false - } - _ => return false - } - guard field(result, "versions") is Array(versions) && versions.length() == 2 else { - return false - } - let completed : Map[String, Bool] = Map([]) - for version in versions { - guard field(version, "version") is String(major) && - ["3", "4"].contains(major) && - !completed.contains(major) && - field(field(version, "parity"), "apiParity") == Json::boolean(true) && - field(field(version, "parity"), "consumer") == Json::boolean(true) && - field(version, "modelSha256") is String(hash) && - hash.length() == 64 && - hash.iter().all(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) && - field(version, "generated") is Array(files) && - !files.is_empty() else { - return false - } - completed[major] = true - } - } - true -} - -///| -async fn main { - let mut json_mode = @env.args().contains("--json") - try { - let schema = @argparse.Command( - "test:cli-extism-python", - about="Qualify a local Python Extism bundle with the pinned shared host, native and JS source/packaged CLIs. Requires existing dependencies, local bundle and independent matching Python API; never downloads or installs them. --json emits one morphir/command-result 0.1.0 document on stdout; progress and child output use stderr. Success returns receiptPath, receiptSha256 and pinPath for fresh checked evidence under .dev/extism/. Failure returns error {code,message} instead of result. Validation exits 1, syntax 2, positive child statuses are preserved. Help uses result.help and performs no builds.", - disable_help_flag=true, - disable_help_subcommand=true, - options=[ - @argparse.OptionArg( - "shared", - about="Absolute finos/morphir checkout at the pinned revision", - ), - @argparse.OptionArg( - "bundle", - about="Absolute corrected Python bundle with release.json and frontend.multiDocument=true", - ), - @argparse.OptionArg( - "python", - about="Absolute Python interpreter with the matching independent morphir-tools API installed", - ), - ], - flags=[@argparse.FlagArg("json"), @argparse.FlagArg("help", short='h')], - ) - let matches = schema.parse(argv=@env.args()[1:]) catch { - error => - raise CommandFailure(2, "extism.invalid_arguments", error.to_string()) - } - json_mode = matches.flags.get("json").unwrap_or(false) - if matches.flags.get("help").unwrap_or(false) { - if json_mode { - println( - ( - { - "profile": "morphir/command-result", - "contractVersion": "0.1.0", - "command": "test:cli-extism-python", - "state": "succeeded", - "exitCode": 0, - "diagnostics": [], - "result": { "help": schema.render_help() }, - } : Json).stringify(), - ) - } else { - println(schema.render_help()) - } - return - } - let shared = @fs.realpath(required(matches, "shared")) - let bundle = @fs.realpath(required(matches, "bundle")) - // Keep a virtualenv's interpreter path: resolving its symlink would lose - // pyvenv.cfg discovery and could accidentally select a different API. - let python = required(matches, "python") - guard @fs.exists(shared + "/.git") && - @fs.exists(bundle + "/release.json") && - @fs.exists(python) else { - raise CommandFailure( - 1, "extism.invalid_input", "Expected a local shared checkout and Python bundle release.json", - ) - } - let declared = @json.parse(@fs.read_file(bundle + "/release.json").text()) - guard field(declared, "artifacts") is Array(artifacts) && - artifacts - .iter() - .find_first(a => field(a, "runtime") == Json::string("wasm")) - is Some(wasm) && - field(field(field(wasm, "claims"), "capabilities"), "frontend") - is Object(frontend) && - frontend.get("multiDocument") == Some(Json::boolean(true)) else { - raise CommandFailure( - 1, "extism.invalid_input", "Full qualification requires a corrected Python bundle declaring frontend.multiDocument=true; older bundles are limited to one source document", - ) - } - qualification_receipt_states() - qualification_receipt_consumers() - checked( - "moon", - [ - "run", "scripts/native-services.mbtx", "--", "--shared", shared, "--json", - ], - environment={ "CARGO_NET_OFFLINE": "true", "RUSTUP_AUTO_INSTALL": "0" }, - ) - let root = @fs.realpath(".") - let host_evidence = @json.parse( - @fs.read_file(root + "/.dev/native-services/evidence.json").text(), - ) - guard host_evidence is Object(evidence) && - evidence.get("target") is Some(String(target)) && - target != "" else { - raise CommandFailure( - 1, "extism.invalid_receipt", "The pinned host build must identify its Rust target before adapter tests", - ) - } - checked( - "cargo", - [ - "+1.98.0", - "test", - "--locked", - "--manifest-path", - root + "/.dev/native-services/Cargo.toml", - "--bin", - "morphir-moonbit-native-services", - "--test", - "shared-restricted-extism", - "--", - "--include-ignored", - "--test-threads=1", - ], - environment={ - "CARGO_NET_OFFLINE": "true", - "RUSTUP_AUTO_INSTALL": "0", - "MORPHIR_NATIVE_TARGET": target, - "MORPHIR_TEST_GUEST": root + - "/_build/native/release/build/finos/morphir-extension/morphir-extension.exe", - "MORPHIR_EXTISM_TEST_BUNDLE": bundle, - }, - ) - checked("moon", ["build", "apps/morphir", "--target", "native"]) - checked("moon", ["build", "apps/morphir", "--target", "native", "--release"]) - checked("bun", ["run", "--cwd", "apps/morphir", "build"]) - checked("node", ["apps/morphir/build-provider/test-extism.mts"]) - for target in ["native", "js"] { - checked("moon", [ - "test", "apps/morphir/cli/extism_wbtest.mbt", "--target", target, - ]) - } - let expected = candidate(root, bundle, python) - let receipt_path = root + "/.dev/extism/qualification.json" - if @fs.exists(receipt_path) { - @fs.remove(receipt_path) - } - checked( - "node", - ["apps/morphir/scripts/test-extism-python.ts"], - environment=Map::from_array([ - ("MORPHIR_EXTISM_BUNDLE", bundle), - ("MORPHIR_EXTISM_PYTHON", python), - ( - "MORPHIR_EXTISM_HOST_PIN", - root + "/.dev/native-services/host-pin.json", - ), - ( - "MORPHIR_EXTISM_NATIVE", - root + "/_build/native/debug/build/morphir/morphir/morphir.exe", - ), - ( - "MORPHIR_EXTISM_RELEASE", - root + "/_build/native/release/build/morphir/morphir/morphir.exe", - ), - ]), - ) - let contents = @fs.read_file(receipt_path) - let bytes = contents.binary() - let receipt = @json.parse(contents.text()) - let pin_path = root + "/.dev/extism/python-pin.json" - let pin = @json.parse(@fs.read_file(pin_path).text()) - guard candidate(root, bundle, python) == expected && - receipt_valid(receipt, expected) && - pin == field(expected, "pin") else { - raise CommandFailure( - 1, "extism.invalid_receipt", "Real-bundle evidence is missing, incomplete, failed or belongs to different candidate bytes", - ) - } - let result : Json = { - "receiptPath": receipt_path, - "receiptSha256": @crypto.bytes_to_hex_string(@crypto.sha256(bytes)), - "pinPath": pin_path, - "receipt": receipt, - } - if json_mode { - println( - ( - { - "profile": "morphir/command-result", - "contractVersion": "0.1.0", - "command": "test:cli-extism-python", - "state": "succeeded", - "exitCode": 0, - "diagnostics": [], - "result": result, - } : Json).stringify(), - ) - } else { - println("Python Extism qualification passed: " + receipt_path) - } - } catch { - error => { - let (status, code, message) = match error { - CommandFailure(status, code, message) => (status, code, message) - _ => (1, "extism.qualification_failed", error.to_string()) - } - @stdio.stderr.write(message + "\n") - if json_mode { - println( - ( - { - "profile": "morphir/command-result", - "contractVersion": "0.1.0", - "command": "test:cli-extism-python", - "state": "failed", - "exitCode": status, - "diagnostics": [], - "error": { "code": code, "message": message }, - } : Json).stringify(), - ) - } - @sys.exit(status) - } - } -} - -///| -fn qualification_fixture() -> (Json, Json) { - let hash = "0".repeat(64) - let modules : Json = { - "extism.mjs": hash, - "supervision.mjs": hash, - "windows-job.mjs": hash, - } - let expected : Map[String, Json] = Map::from_array([ - ("platform", Json::string("darwin")), - ("architecture", Json::string("arm64")), - ("node", Json::string("v24.22.1")), - ("runtimeSha256", Json::string(hash)), - ("python", { "executable": "/python", "sha256": hash }), - ( - "host", - { "executable": "/host", "sha256": hash, "sourceRevision": "approved" }, - ), - ("hostBuild", { "sourceRevision": "approved", "cargoLockSha256": hash }), - ("bundle", { "descriptorSha256": hash, "wasmSha256": hash }), - ("sourceRevision", Json::string("candidate")), - ("sourceFiles", { "source": hash }), - ("checkDefinitionSha256", Json::string(hash)), - ("helpers", modules), - ]) - let receipt = expected.copy() - receipt.remove("helpers") - receipt["profile"] = Json::string("morphir/extism-python-qualification") - receipt["contractVersion"] = Json::string("0.1.0") - receipt["state"] = Json::string("succeeded") - let launchers : Map[String, Json] = Map([]) - let results : Array[Json] = [] - for name in ["source-native", "source-js", "packaged-native", "packaged-npm"] { - let js = name == "source-js" || name == "packaged-npm" - launchers[name] = { - "programSha256": hash, - "entrypointSha256": if js { - Json::string(hash) - } else { - Json::null() - }, - } - results.push({ - "name": name, - "programSha256": hash, - "helpers": modules, - "entrypoint": if js { - [{ "file": "/bin/morphir.js", "sha256": hash }] - } else { - [] - }, - "versions": ["3", "4"].map(major => { - ( - { - "version": major, - "parity": { "apiParity": true, "consumer": true }, - "modelSha256": hash, - "generated": ["models.py"], - } : Json) - }), - }) - } - expected["launchers"] = Json::object(launchers) - receipt["results"] = Json::array(results) - (Json::object(receipt), Json::object(expected)) -} - -///| -fn replace_field(value : Json, key : String, replacement : Json) -> Json { - guard value is Object(fields) else { panic() } - let changed = fields.copy() - changed[key] = replacement - Json::object(changed) -} - -///| -fn qualification_receipt_states() -> Unit raise { - let (receipt, expected) = qualification_fixture() - assert_true(receipt_valid(receipt, expected)) - for state in ["failed", "skipped", "pending"] { - assert_false( - receipt_valid( - replace_field(receipt, "state", Json::string(state)), - expected, - ), - ) - } - for - key in [ - "host", "hostBuild", "sourceRevision", "sourceFiles", "checkDefinitionSha256", - "platform", "architecture", "node", "runtimeSha256", "python", "bundle", - ] { - assert_false( - receipt_valid( - replace_field(receipt, key, Json::string("different candidate")), - expected, - ), - ) - } -} - -///| -fn qualification_receipt_consumers() -> Unit raise { - let (receipt, expected) = qualification_fixture() - guard field(receipt, "results") is Array(rows) else { panic() } - assert_false( - receipt_valid( - replace_field(receipt, "results", Json::array(rows[:3].to_owned())), - expected, - ), - ) - let duplicated = rows.copy() - duplicated[3] = duplicated[0] - assert_false( - receipt_valid( - replace_field(receipt, "results", Json::array(duplicated)), - expected, - ), - ) - guard field(rows[0], "versions") is Array(versions) else { panic() } - for - bad_version in [ - replace_field(versions[1], "version", Json::string("3")), - replace_field(versions[1], "parity", { - "apiParity": true, - "consumer": false, - }), - replace_field(versions[1], "parity", { - "apiParity": false, - "consumer": true, - }), - replace_field(versions[1], "modelSha256", Json::string("g".repeat(64))), - replace_field(versions[1], "generated", []), - ] { - let changed = rows.copy() - changed[0] = replace_field(rows[0], "versions", [versions[0], bad_version]) - assert_false( - receipt_valid( - replace_field(receipt, "results", Json::array(changed)), - expected, - ), - ) - } - let changed = rows.copy() - changed[1] = replace_field(rows[1], "entrypoint", [ - { "file": "/bin/morphir.js", "sha256": "other bytes" }, - ]) - assert_false( - receipt_valid( - replace_field(receipt, "results", Json::array(changed)), - expected, - ), - ) -} - -///| -test "qualification refuses failed, skipped and stale candidate evidence" { - qualification_receipt_states() -} - -///| -test "qualification requires all four launchers and both successful IR consumers" { - qualification_receipt_consumers() -} diff --git a/scripts/portable-mep-cli.mbtx b/scripts/portable-mep-cli.mbtx deleted file mode 100644 index f52fc52a1..000000000 --- a/scripts/portable-mep-cli.mbtx +++ /dev/null @@ -1,708 +0,0 @@ ----- -import { - "moonbitlang/async@0.22.7", - "moonbitlang/async@0.22.7/fs", - "moonbitlang/async@0.22.7/process", - "moonbitlang/async@0.22.7/stdio", - "moonbitlang/core/argparse", - "moonbitlang/core/env", - "moonbitlang/x@0.5.5/sys", - "moonbitlang/core/json", - "moonbitlang/core/encoding/base64", - "moonbitlang/core/encoding/utf8", - "moonbitlang/x@0.5.5/crypto", -} ----- - -// Primary-CLI acceptance for the saved core-Wasm extension route. The real -// `morphir extension` command dispatches the packaged guest through the pinned -// Node host; the independent shared kit owns the source and consumer verdict. - -///| -let host_modules : Array[String] = [ - "node-mep-cli.mjs", "node-mep-stdio.mjs", "node-mep-adapter.mjs", "node-mep-worker.mjs", - "mep-adapter.mjs", "mep-frames.mjs", "wasm-memory.mjs", "node-artifacts.mjs", "artifacts.mjs", - "assembly.mjs", "index.mjs", "dist/bridge.mjs", -] - -///| -fn sha256(bytes : Bytes) -> String { - @crypto.bytes_to_hex_string(@crypto.sha256(bytes)) -} - -///| -async fn digest(path : String) -> String { - sha256(@fs.read_file(path).binary()) -} - -///| -fn field(value : Json, name : String) -> Json { - match value { - Object(fields) => fields.get(name).unwrap_or(Json::null()) - _ => Json::null() - } -} - -///| -async fn checked( - program : String, - args : Array[String], - cwd~ : String = ".", -) -> String { - let (code, out, err) = @process.collect_output(program, args, cwd~) - guard code == 0 else { - if json_output.val { - @stdio.stderr.write(out.text() + err.text()) - raise ChildFailed( - if code > 0 { - code - } else { - 1 - }, - "Child process failed: " + program, - ) - } - @stdio.stderr.write(out.text() + err.text()) - fail( - "Portable MEP CLI acceptance failed: " + program + " " + args.join(" "), - ) - } - out.text() -} - -///| -/// One primary CLI build: the npm launcher or the native executable. -priv struct CliHost { - name : String - program : String - prefix : Array[String] - controls : Array[String] -} - -///| -priv struct Run { - code : Int32 - report : Json - stderr : String -} - -///| -/// Every invocation uses an owned TMPDIR, which must be empty afterwards: the -/// command removes its session files and the guest wrote nothing of its own. -async fn cli(host : CliHost, scratch : String, args : Array[String]) -> Run { - let tmp = scratch + "/tmp" - let (code, out, err) = @process.collect_output( - host.program, - host.prefix + ["--json"] + args + host.controls, - extra_env={ "TMPDIR": tmp }, - cwd=scratch, - ) - guard @fs.readdir(tmp, include_hidden=true).is_empty() else { - fail(host.name + " left session files: " + args.join(" ")) - } - let report = @json.parse(out.text()) catch { - _ => - fail(host.name + " returned no JSON report: " + out.text() + err.text()) - } - { code, report, stderr: err.text(), } -} - -///| -fn expect( - run : Run, - code : Int32, - error : String, - label : String, -) -> Unit raise { - let observed = match field(run.report, "error") { - String(text) => text - _ => "" - } - guard run.code == code && observed.contains(error) else { - fail( - label + - ": expected exit " + - code.to_string() + - " and error '" + - error + - "', observed " + - run.code.to_string() + - " " + - run.report.stringify() + - run.stderr, - ) - } -} - -///| -fn expect_completed_session(run : Run, label : String) -> Unit raise { - expect(run, 0, "", label) - guard field(run.report, "state") == Json("completed") && - field(field(run.report, "lifecycle"), "exitCode") == Json(0) && - field(field(run.report, "lifecycle"), "workDirectoryEmpty") == Json(true) else { - fail(label + ": incomplete lifecycle " + run.report.stringify()) - } -} - -///| -async fn write_pin( - path : String, - node : String, - host_root : String, - artifacts : String, - binding~ : String = "core-wasm-process", - wasm~ : String? = None, -) -> Json { - let modules : Map[String, Json] = Map([]) - for name in host_modules { - modules[name] = Json::string(digest(host_root + "/" + name)) - } - let pin : Json = { - "contractVersion": "0.1.0-draft.1", - "binding": binding, - "executionProfile": { - "id": "morphir/mep-bytes-v1/v1-draft", - "sha256": "6ed5e96fa9c7f7dcaa179efc88e517307463cfeddc5e2eeae854f0395e7a4ec6", - }, - "runtime": { - "name": "node", - "version": "v24.22.1", - "executable": node, - "sha256": digest(node), - }, - "host": { "root": host_root, "modules": Json::object(modules) }, - "artifact": { - "directory": artifacts, - "wasmSha256": wasm.unwrap_or(digest(artifacts + "/model.wasm")), - "manifestSha256": digest(artifacts + "/manifest.ion"), - }, - "origin": "local package:mep-extension output qualified by portable-mep-cli.mbtx", - } - @fs.write_file(path, pin.stringify() + "\n") - pin -} - -///| -/// The Boolean source comes from the shared kit, unchanged. -fn kit_source(kit : Json) -> (String, String) raise { - let source = field(kit, "source") - guard field(field(source, "package"), "name") is String(package_name) && - field(field(source, "sources"), "documents") is Array(documents) && - documents.length() == 1 && - field(documents[0], "text") is String(text) else { - fail("Shared kit has no single-document source") - } - (package_name, text) -} - -///| -/// A Morphir JSON envelope around the kit's independent plain IR. -fn json_envelope(kit : Json) -> Json { - let bytes = @utf8.encode(field(kit, "plainIr").stringify()) - let hash = sha256(bytes) - { - "envelopeVersion": "0.1.0-draft.1", - "modelId": "urn:morphir:mck:bool", - "revision": "sha256:" + hash, - "irVersion": "4.0.0", - "profile": "morphir-ir", - "payload": { - "codec": "morphir-json", - "layout": "single-document", - "decodedByteLength": bytes.length(), - "sha256": hash, - "content": @base64.encode(bytes[:]), - }, - } -} - -///| -async fn expect_artifacts( - directory : String, - expected : Json, - label : String, -) -> Unit { - guard expected is Object(files) && !files.is_empty() else { - fail("Shared kit lacks " + label) - } - for name, content in files { - guard content is String(text) && - @fs.read_file(directory + "/" + name).text() == text else { - fail(label + " differs from the shared kit: " + name) - } - } -} - -///| -/// The shared kit owns consumer source and expected stdout; the pinned -/// compiler builds the exact library returned through the CLI. -async fn consumer( - root : String, - scratch : String, - generated : String, - kit : Json, -) -> String { - guard field(field(kit, "consumer"), "files") is Object(files) && - field(field(kit, "consumer"), "expectedStdout") is String(expected) else { - fail("Missing independent consumer fixture") - } - let directory = scratch + "/consumer" - @fs.mkdir(directory) - for name, content in files { - guard content is String(text) else { fail("Invalid consumer source") } - @fs.write_file(directory + "/" + name, text) - } - @fs.write_file( - scratch + "/moon.work", - "members = " + - Json([generated, directory, root + "/pkgs/morphir-sdk"]).stringify() + - "\n", - ) - let result = checked("moon", [ - "-C", scratch, "run", "--target", "native", "consumer", - ]) - guard result == expected else { - fail("Shared consumer expected stdout differs") - } - result -} - -///| -async fn exercise( - root : String, - evidence : String, - host : CliHost, - kit : Json, - pins : Map[String, String], -) -> Json { - let scratch = evidence + "/" + host.name - @fs.mkdir(scratch) - @fs.mkdir(scratch + "/tmp") - @fs.mkdir(scratch + "/src") - let (package_name, text) = kit_source(kit) - @fs.write_file(scratch + "/src/main.mbt", text) - @fs.write_file(scratch + "/src/broken.mbt", "pub fn {") - @fs.write_file( - scratch + "/json-envelope.json", - json_envelope(kit).stringify(), - ) - let pin = pins.get("valid").unwrap() - let cases : Array[Json] = [] - let record = fn(name : String, run : Run) { - cases.push({ - "case": name, - "exitCode": run.code, - "state": field(run.report, "state"), - "error": field(run.report, "error"), - }) - } - let compile = fn(source : String, output : String, extra : Array[String]) { - [ - "extension", "compile", source, "--extension-pin", pin, "--package", package_name, - "--output", output, - ] + - extra - } - // Discovery: identity, claims and truthful runtime access. - let described = cli(host, scratch, [ - "extension", "describe", "--extension-pin", pin, - ]) - expect_completed_session(described, "describe") - guard field(field(described.report, "extension"), "id") == - Json("morphir-moonbit") && - field( - field(field(described.report, "result"), "capabilities"), - "cancellation", - ) == - Json(false) && - field(field(described.report, "access"), "wireCancellation") == Json(false) else { - fail("describe claims differ: " + described.report.stringify()) - } - record("describe", described) - // Guest-level refusals keep exact diagnostics and publish nothing. - let lossy = cli( - host, - scratch, - compile("src/main.mbt", "lossy", ["--model-format", "json"]), - ) - expect(lossy, 1, "extension.operation_unsuccessful", "lossy JSON compile") - guard field(lossy.report, "result") - .stringify() - .contains("data.lossy_conversion") && - !@fs.exists(scratch + "/lossy") else { - fail("JSON compile did not refuse lossy conversion explicitly") - } - record("compile-json-lossy-refusal", lossy) - let broken = cli(host, scratch, compile("src/broken.mbt", "broken", [])) - expect(broken, 1, "extension.operation_unsuccessful", "malformed source") - guard !@fs.exists(scratch + "/broken") else { - fail("Failed compilation published output") - } - record("compile-malformed-source", broken) - let retired = cli( - host, - scratch, - compile("src/main.mbt", "retired", [ - "--frontend-profile", "moonbit-model-bool-library-v1", - ]), - ) - expect(retired, 1, "extension.operation_unsuccessful", "retired profile") - guard field(retired.report, "result") - .stringify() - .contains("moonbit_frontend.unsupported_profile") else { - fail("Retired profile was not refused by the guest") - } - record("compile-retired-profile", retired) - // A fresh session after those failures compiles and generates end to end. - let compiled = cli(host, scratch, compile("src/main.mbt", "model", [])) - expect_completed_session(compiled, "compile") - guard field(field(field(compiled.report, "negotiated"), "tuple"), "codec") == - Json("ion-binary") && - field(field(compiled.report, "lifecycle"), "shutdown") == - Json("acknowledged") else { - fail("compile did not negotiate the Ion tuple") - } - record("compile-ion", compiled) - let generated = cli(host, scratch, [ - "extension", "generate", "model/model-envelope.json", "--extension-pin", pin, - "--output", "generated", - ]) - expect_completed_session(generated, "generate") - guard @fs.read_file(scratch + "/generated/mep/source-model.ionb").binary() == - @fs.read_file(scratch + "/model/model.ionb").binary() else { - fail("Generation did not retain the exact Ion model bytes") - } - record("generate-ion-retained", generated) - let result = consumer(root, scratch, scratch + "/generated", kit) - // Supported Morphir JSON generation matches the kit's expected artifacts. - let json = cli(host, scratch, [ - "extension", "generate", "json-envelope.json", "--extension-pin", pin, "--output", - "json-generated", - ]) - expect_completed_session(json, "generate JSON") - expect_artifacts( - scratch + "/json-generated", - field(kit, "expectedJsonArtifacts"), - "JSON generation", - ) - record("generate-json", json) - // Integrity: the guest refuses a payload whose digest does not match. - let envelope = @fs.read_file(scratch + "/model/model-envelope.json").text() - let digest_text = match - field(field(@json.parse(envelope), "payload"), "sha256") { - String(text) => text - _ => fail("Missing payload digest") - } - @fs.write_file( - scratch + "/tampered.json", - envelope.replace(old=digest_text, new="0".repeat(64)), - ) - let tampered = cli(host, scratch, [ - "extension", "generate", "tampered.json", "--extension-pin", pin, "--output", - "tampered", - ]) - guard tampered.code == 1 && !@fs.exists(scratch + "/tampered") else { - fail("Tampered model was not refused: " + tampered.report.stringify()) - } - record("generate-digest-refusal", tampered) - // Local refusals happen before any process starts. - @fs.write_file( - scratch + "/mixed.json", - envelope.replace( - old="\"profile\":\"morphir-model\"", - new="\"profile\":\"morphir-ir\"", - ), - ) - let mixed = cli(host, scratch, [ - "extension", "generate", "mixed.json", "--extension-pin", pin, "--output", "mixed", - ]) - expect(mixed, 2, "extension.unsupported_model_tuple", "mixed tuple") - record("generate-unsupported-tuple", mixed) - let exists = cli(host, scratch, compile("src/main.mbt", "model", [])) - expect(exists, 2, "extension.output_exists", "existing output") - record("existing-output-refusal", exists) - for - (name, error) in [ - ("wrong-wasm", "extension.identity_mismatch"), - ("component", "extension.unsupported_binding"), - ] { - let refused = cli(host, scratch, [ - "extension", - "describe", - "--extension-pin", - pins.get(name).unwrap(), - ]) - expect(refused, 2, error, name) - record("pin-" + name, refused) - } - @fs.write_file(scratch + "/cancel", "") - let cancelled = cli(host, scratch, [ - "extension", "describe", "--extension-pin", pin, "--cancel-file", "cancel", - ]) - expect(cancelled, 130, "execution.cancelled", "cancel file") - record("cancelled", cancelled) - let deadline = cli(host, scratch, [ - "extension", "describe", "--extension-pin", pin, "--timeout", "1", - ]) - expect(deadline, 124, "execution.deadline", "deadline") - record("deadline", deadline) - { - "host": host.name, - "cases": Json::array(cases), - "consumerResult": result, - "modelSha256": digest(scratch + "/model/model.ionb"), - "librarySha256": digest(scratch + "/generated/library.mbt"), - } -} - -///| -async fn execute() -> Json { - guard @env.get_env_var("MORPHIR_MEP_KIT") is Some(kit_path) && - @env.get_env_var("MORPHIR_MEP_KIT_SHA256") is Some(kit_digest) else { - fail( - "Set MORPHIR_MEP_KIT and MORPHIR_MEP_KIT_SHA256 to the shared guest kit", - ) - } - let kit_path = @fs.realpath(kit_path) - guard digest(kit_path) == kit_digest else { - fail("Shared kit does not match its SHA-256 pin") - } - let kit = @json.parse(@fs.read_file(kit_path).text()) - let root = @fs.realpath(".") - let artifacts = @fs.realpath( - @env.get_env_var("MORPHIR_MEP_ARTIFACTS").unwrap_or(".dev/portable-mep") + - "/extension", - ) - let evidence = @env.get_env_var("MORPHIR_MEP_CLI_EVIDENCE").unwrap_or( - ".dev/portable-mep-cli", - ) - guard !@fs.exists(evidence) else { - fail("Choose a new CLI evidence directory to preserve earlier results") - } - let compiler = checked("moon", ["version", "--all"]) - guard compiler.contains("0.10.18+6c83a7f62") else { - fail("Pinned MoonBit 0.10.18 is required") - } - let node = @fs.realpath( - checked("node", ["-p", "process.execPath"]).trim().to_owned(), - ) - guard checked(node, ["--version"]).trim() == "v24.22.1" else { - fail("Node 24.22.1 is required") - } - // Build every host byte first; the pin is taken over the final outputs. - ignore( - checked("bun", ["run", "--cwd", "pkgs/morphir-host/embedding", "build"]), - ) - ignore(checked("bun", ["run", "--cwd", "apps/morphir", "build"])) - ignore( - checked("moon", ["build", "apps/morphir", "--target", "native", "--release"]), - ) - @fs.mkdir(evidence, recursive=true) - let evidence = @fs.realpath(evidence) - let host_root = root + "/pkgs/morphir-host/embedding" - let pin = write_pin(evidence + "/pin.json", node, host_root, artifacts) - ignore( - write_pin( - evidence + "/pin-wrong-wasm.json", - node, - host_root, - artifacts, - wasm=Some("0".repeat(64)), - ), - ) - ignore( - write_pin( - evidence + "/pin-component.json", - node, - host_root, - artifacts, - binding="component-model", - ), - ) - let pins : Map[String, String] = { - "valid": evidence + "/pin.json", - "wrong-wasm": evidence + "/pin-wrong-wasm.json", - "component": evidence + "/pin-component.json", - } - let launcher = root + "/apps/morphir/bin/morphir.js" - let native = root + "/_build/native/release/build/morphir/morphir/morphir.exe" - let helper = root + "/apps/morphir/build-provider/extension.mjs" - let identities : Json = { - "launcher": digest(launcher), - "compiledCli": digest(root + "/apps/morphir/dist/morphir.js"), - "nativeCli": digest(native), - "helper": digest(helper), - "supervision": digest(root + "/apps/morphir/build-provider/supervision.mjs"), - "identity": digest(root + "/apps/morphir/build-provider/identity.mjs"), - } - let hosts = [ - { name: "js", program: node, prefix: [launcher], controls: [], }, - { - name: "native", - program: native, - prefix: [], - controls: ["--extension-helper", helper, "--node", node], - }, - ] - let results : Array[Json] = [] - for host in hosts { - results.push(exercise(root, evidence, host, kit, pins)) - } - // The same pinned bytes produced the same model and library on both hosts. - guard field(results[0], "modelSha256") == field(results[1], "modelSha256") && - field(results[0], "librarySha256") == field(results[1], "librarySha256") else { - fail("JS and native CLI hosts returned different results") - } - guard write_pin(evidence + "/pin-after.json", node, host_root, artifacts) == - pin && - digest(kit_path) == kit_digest else { - fail("Pinned inputs changed during qualification") - } - @fs.remove(evidence + "/pin-after.json") - let receipt : Json = { - "scope": "primary MoonBit CLI explicit-fixture extension route; installed bundle selection is separate", - "route": "core-wasm-process: saved core Wasm guest in the pinned Node process host; not a Component Model binding", - "sourceRevision": checked("git", ["rev-parse", "HEAD"]).trim().to_owned(), - "sourceDirty": checked("git", ["status", "--porcelain"]).trim() != "", - "compiler": compiler, - "kitSha256": kit_digest, - "pin": pin, - "cli": identities, - "hosts": Json::array(results), - } - @fs.write_file( - evidence + "/receipt.ion", - "portable_mep_cli::" + receipt.stringify() + "\n", - ) - (if json_output.val { @stdio.stderr } else { @stdio.stdout }).write( - "Primary CLI extension route passed on JS and native hosts with the shared consumer" + - "\n", - ) - receipt -} - -///| -let json_output : Ref[Bool] = Ref(false) - -///| -suberror CommandFailure { - ChildFailed(Int32, String) -} - -///| -fn legacy_command() -> @argparse.Command { - @argparse.Command( - "portable-mep-cli.mbtx", - about="Run the saved MEP guest through the primary MoonBit CLI extension command", - ) -} - -///| -fn json_command() -> @argparse.Command { - @argparse.Command( - "portable-mep-cli.mbtx", - about="Run the saved MEP guest through the primary MoonBit CLI extension command. Leading --json selects JSON mode. JSON emits one compact morphir/command-result 0.1.0 value plus LF on stdout after completion; progress goes to stderr. Success has result and diagnostics; failure has error.code/message and exitCode matching process status. Expected refusals and legacy child-failure branches exit 1; error.message retains the child status. Default abort presentation is unchanged. Result fields scope, route, sourceRevision, sourceDirty, compiler, kitSha256, pin, cli and hosts describe completed primary CLI acceptance. A leading -- protects a literal --json in legacy argv.", - disable_help_flag=true, - disable_help_subcommand=true, - subcommands=[ - @argparse.Command( - "help", - disable_help_flag=true, - disable_help_subcommand=true, - flags=[@argparse.FlagArg("help", short='h')], - ), - ], - flags=[ - @argparse.FlagArg("json", about="Emit the completed command result"), - @argparse.FlagArg( - "help", - short='h', - about="Return render_help without effects", - ), - ], - ) -} - -///| -fn wants_json(argv : ArrayView[String]) -> Bool { - // Only the leading control is reserved. The library owns its syntax; all - // other legacy argv stays untouched, including literal --json after --. - let prefix = argv[:if argv.is_empty() { 0 } else { 1 }] - let matches = @argparse.Command( - "portable-mep-cli.mbtx", - disable_help_flag=true, - disable_help_subcommand=true, - flags=[@argparse.FlagArg("json", required=true)], - ).parse(argv=prefix) catch { - _ => return false - } - matches.flags.get_or_default("json", false) -} - -///| -fn success(result : Json) -> Json { - { - "profile": "morphir/command-result", - "contractVersion": "0.1.0", - "command": "portable.mep.cli", - "state": "succeeded", - "exitCode": 0, - "result": result, - "diagnostics": [], - } -} - -///| -async fn main { - let argv = @env.args()[1:] - json_output.val = wants_json(argv) - if !json_output.val { - ignore( - @argparse.Command( - "portable-mep-cli.mbtx", - about="Run the saved MEP guest through the primary MoonBit CLI extension command", - ).parse(argv=@env.args()[1:], env=Map([])), - ) catch { - error => fail(error.to_string()) - } - ignore(execute()) - return - } - let result : Json = try { - let matches = json_command().parse(argv~) - if matches.flags.get_or_default("help", false) || - matches.subcommand is Some(_) { - { - "render_help": legacy_command().render_help(), - "json_help": json_command().render_help(), - } - } else { - execute() - } - } catch { - error => { - let (code, message, kind) = match error { - ChildFailed(code, message) => - ( - 1, - message + " (child exit " + code.to_string() + ")", - "child.failed", - ) - _ => (1, error.to_string(), "command.failed") - } - let failure : Json = { - "profile": "morphir/command-result", - "contractVersion": "0.1.0", - "command": "portable.mep.cli", - "state": "failed", - "exitCode": code, - "error": { "code": kind, "message": message }, - "diagnostics": [], - } - @stdio.stdout.write(failure.stringify() + "\n") - @sys.exit(code) - return - } - } - @stdio.stdout.write(success(result).stringify() + "\n") -} diff --git a/tests/wasm-host/README.md b/tests/wasm-host/README.md index c82b7489a..7771394f2 100644 --- a/tests/wasm-host/README.md +++ b/tests/wasm-host/README.md @@ -174,7 +174,7 @@ Browser and native component-runtime candidates are optional for that first rout | Production JS embedding in Node/Chromium | Direct components refused | The suite passes actual component bytes to `createEmbeddedToolchain().request({operation:'run', ...})` and requires `WebAssembly.CompileError`. | | Wasmoon public native facade, pin 5490d6b | Historical values pass; resource/callback gaps | This current run is not-run for native. Do not apply the historical result to another checkout/version/platform. | | Canonical Extism public loader, Extism 1.30.0 / Wasmtime 43.0.2 | Components refused during module parsing | The same fixture component is validated and runs in the reference host. A core control reaches the separate missing Extism guest-info export. This does not qualify the core fixture as an Extism guest. | -| Primary MoonBit CLI | Component binding not integrated; saved core-Wasm route dispatched | `morphir extension` dispatches only the pinned `core-wasm-process` binding, the saved core-Wasm guest in the Node process host, and refuses component bindings. `mise run test:portable-mep-cli` qualifies it on the JS and native CLI builds. See `apps/morphir/docs/extension.md`. | +| Primary MoonBit CLI | Component binding not integrated; core-Wasm guests run on the worker host | `morphir extension` runs core-Wasm guests with the `morphir-bytes-v1` or `extism-kernel-v1` ABI on its own worker host and refuses components. `mise run test:portable-mep-cli` qualifies the saved portable guest from the native and npm CLIs. See `apps/morphir/docs/extension.md`. | | Workbench/Proton application paths | Not qualified for components by this suite | The Chromium fixture is a harness, not the shipped desktop/web application's load/invoke path. | The shared canonical host is inspected independently at finos/morphir diff --git a/tools/portable-command-json.test.ts b/tools/portable-command-json.test.ts index 4520b53d6..34f374fbb 100644 --- a/tools/portable-command-json.test.ts +++ b/tools/portable-command-json.test.ts @@ -6,7 +6,7 @@ import { dirname, join, resolve, sep } from 'node:path'; // This is a command-output regression, not a runtime, MEP, codec or MCK // qualification. Its children are deliberately synthetic native MoonBit programs. const names = ['codec-adapter-mck', 'package-codec-adapter', 'package-mep-extension', - 'portable-mep-check', 'portable-mep-cli', 'portable-mep-mck'] as const; + 'portable-mep-check', 'portable-mep-mck'] as const; type Name = typeof names[number]; const hostModules = ['node-mep-cli.mjs', 'node-mep-stdio.mjs', 'node-mep-adapter.mjs', 'node-mep-worker.mjs', 'mep-adapter.mjs', 'mep-frames.mjs', 'wasm-memory.mjs', 'node-artifacts.mjs', 'artifacts.mjs', @@ -244,7 +244,7 @@ async function regression() { PATH: bin, MOON_HOME: 'fixture preserved moon home 雪', PORTABLE_JOURNAL: journal, PORTABLE_CORE: 'fixture core --json 雪', MORPHIR_CODEC_ARTIFACTS: artifactsPath, MORPHIR_CODEC_EVIDENCE: join(cwd, 'codec-evidence'), MORPHIR_MEP_ARTIFACTS: artifactsPath, MORPHIR_MEP_EVIDENCE: join(cwd, 'local-evidence'), - MORPHIR_MEP_MCK_EVIDENCE: join(cwd, 'mck-evidence'), MORPHIR_MEP_CLI_EVIDENCE: join(cwd, 'cli-evidence'), + MORPHIR_MEP_MCK_EVIDENCE: join(cwd, 'mck-evidence'), MORPHIR_MCK_BIN: runner, MORPHIR_MCK_SHA256: fixtureHash, MORPHIR_MEP_KIT: kitPath, MORPHIR_MEP_KIT_SHA256: sha(readFileSync(kitPath)), MORPHIR_WORKBENCH_KIT: workbench, }; @@ -309,8 +309,8 @@ async function regression() { assert.equal(readFileSync(join(result.result.destination, 'recipe.txt'), 'utf8'), 'fixture recipe\n'); assert.deepEqual(launches.map(entry => entry.program), ['moon', 'moon', 'moon', 'bun', 'node']); } else { - const ion = name === 'portable-mep-cli' ? 'receipt.ion' : name === 'portable-mep-check' ? 'portable-receipt.ion' : 'qualification.ion'; - const directory = name === 'portable-mep-cli' ? 'cli-evidence' : name === 'portable-mep-check' ? 'local-evidence' : name === 'portable-mep-mck' ? 'mck-evidence' : 'codec-evidence'; + const ion = name === 'portable-mep-check' ? 'portable-receipt.ion' : 'qualification.ion'; + const directory = name === 'portable-mep-check' ? 'local-evidence' : name === 'portable-mep-mck' ? 'mck-evidence' : 'codec-evidence'; const text = readFileSync(join(success.cwd, directory, ion), 'utf8'); assert.deepEqual(JSON.parse(text.slice(text.indexOf('::') + 2)), result.result); } @@ -322,20 +322,6 @@ async function regression() { const acceptance = launches.find(entry => entry.program === 'acceptance')!; assert.ok(acceptance); assert.ok(acceptance.args[1] === success.cwd); } - if (name === 'portable-mep-cli') { - const calls = launches.filter(entry => entry.args.includes('extension')); - assert.equal(calls.length, 28); assert.ok(calls.every(entry => entry.args[0] === '--json' || entry.args[1] === '--json')); - for (const entry of calls) { - assert.ok(entry.program === 'node' || entry.program === 'morphir'); - const backend = entry.program === 'node' ? 'js' : 'native'; - const evidence = success.env['MORPHIR_MEP_CLI_EVIDENCE']!; - assert.equal(entry.cwd, join(evidence, backend)); - // The wrapper forwards this literal string; the OS normalizes cwd - // separately. Keep exact environment bytes, including mixed slashes. - assert.equal(entry.tmpdir, `${evidence}/${backend}/tmp`); - } - assert.ok(result.result.hosts.every((host: { cases: unknown[] }) => host.cases.length === 14)); - } for (const code of [23, 130]) { const failure = fixture(); failure.env['PORTABLE_FAIL_PROGRAM'] = 'moon'; failure.env['PORTABLE_FAIL_CODE'] = String(code); const failed = run(name, ['--json'], failure); @@ -363,8 +349,8 @@ async function regression() { rmSync(join(destination, 'core-graph.json'), { force: true }); mkdirSync(join(destination, 'core-graph.json')); } else { - const directory = name === 'codec-adapter-mck' ? 'codec-evidence' : name === 'portable-mep-check' ? 'local-evidence' : name === 'portable-mep-cli' ? 'cli-evidence' : 'mck-evidence'; - const file = name === 'portable-mep-check' ? 'portable-receipt.ion' : name === 'portable-mep-cli' ? 'receipt.ion' : 'qualification.ion'; + const directory = name === 'codec-adapter-mck' ? 'codec-evidence' : name === 'portable-mep-check' ? 'local-evidence' : 'mck-evidence'; + const file = name === 'portable-mep-check' ? 'portable-receipt.ion' : 'qualification.ion'; writeFailure.env['PORTABLE_BLOCK_FILE'] = join(writeFailure.cwd, directory, file); } decoded(run(name, ['--json'], writeFailure), 'failed', 1); @@ -372,7 +358,6 @@ async function regression() { const refusal = fixture(); if (name.startsWith('package-')) refusal.env['PORTABLE_FAIL_PROGRAM'] = 'missing'; else if (name === 'codec-adapter-mck') delete refusal.env['MORPHIR_MCK_BIN']; - else if (name === 'portable-mep-cli') refusal.env['MORPHIR_MEP_KIT_SHA256'] = 'wrong'; else if (name === 'portable-mep-mck') refusal.env['MORPHIR_MCK_SHA256'] = 'wrong'; else mkdirSync(refusal.env['MORPHIR_MEP_EVIDENCE']!); if (!name.startsWith('package-')) { decoded(run(name, ['--json'], refusal), 'failed', 1); assert.equal(journal(refusal).length, 0); } @@ -380,14 +365,12 @@ async function regression() { decoded(run(name, ['--json'], missing), 'failed', 1); // Legacy wrappers ignored operands. Neither variadic parsing nor new JSON // dispatch may reinterpret an escaped flag or rebuild a foreign tail. - if (name !== 'portable-mep-cli') { - const defaults = fixture(); - const legacy = run(name, ['--', '--json', '', '雪', '--', '--help', '--json=literal'], defaults); - assert.equal(legacy.code, 0, legacy.stdout + legacy.stderr); - assert.throws(() => JSON.parse(legacy.stdout)); - const escaped = fixture(); - decoded(run(name, ['--json', '--', '--json', '', '雪', '--', '--help'], escaped), 'succeeded', 0); - } + const defaults = fixture(); + const legacy = run(name, ['--', '--json', '', '雪', '--', '--help', '--json=literal'], defaults); + assert.equal(legacy.code, 0, legacy.stdout + legacy.stderr); + assert.throws(() => JSON.parse(legacy.stdout)); + const escaped = fixture(); + decoded(run(name, ['--json', '--', '--json', '', '雪', '--', '--help'], escaped), 'succeeded', 0); const legacyFailure = run(name, [], writeFailure); assert.notEqual(legacyFailure.code, 0, 'Default failure retains its backend presentation'); const baseline = target === 'native' ? process.env['PORTABLE_JSON_BASELINE'] : undefined; @@ -405,9 +388,9 @@ async function regression() { if (entry.isDirectory()) snapshot(file); else snapshots.set(file, readFileSync(file)); } } - for (const key of ['MORPHIR_CODEC_EVIDENCE', 'MORPHIR_MEP_EVIDENCE', 'MORPHIR_MEP_MCK_EVIDENCE', 'MORPHIR_MEP_CLI_EVIDENCE']) snapshot(f.env[key]!); + for (const key of ['MORPHIR_CODEC_EVIDENCE', 'MORPHIR_MEP_EVIDENCE', 'MORPHIR_MEP_MCK_EVIDENCE']) snapshot(f.env[key]!); if (name.startsWith('package-')) for (const file of ['core-graph.json', 'recipe.txt']) snapshots.set(join(f.env['MORPHIR_CODEC_ARTIFACTS']!, file), readFileSync(join(f.env['MORPHIR_CODEC_ARTIFACTS']!, file))); - for (const key of ['MORPHIR_CODEC_EVIDENCE', 'MORPHIR_MEP_EVIDENCE', 'MORPHIR_MEP_MCK_EVIDENCE', 'MORPHIR_MEP_CLI_EVIDENCE']) rmSync(f.env[key]!, { recursive: true, force: true }); + for (const key of ['MORPHIR_CODEC_EVIDENCE', 'MORPHIR_MEP_EVIDENCE', 'MORPHIR_MEP_MCK_EVIDENCE']) rmSync(f.env[key]!, { recursive: true, force: true }); const original = artifacts.get(name)!; artifacts.set(name, join(baseline, name + '.exe')); const old = run(name, [], f); @@ -420,10 +403,6 @@ async function regression() { assert.equal(baselineFailure.code, legacyFailure.code); assert.equal(baselineFailure.signal, legacyFailure.signal); artifacts.set(name, original); - if (name === 'portable-mep-cli') { - const now = run(name, ['--help'], f); artifacts.set(name, join(baseline, name + '.exe')); - assert.deepEqual(run(name, ['--help'], f), now); artifacts.set(name, original); - } } console.log(`${name}: completed result, help, refusals, child 23/130, channels and legacy operands passed`); }