diff --git a/.github/workflows/extension-libraries-upstream.yml b/.github/workflows/extension-libraries-upstream.yml new file mode 100644 index 0000000000..21d2d40510 --- /dev/null +++ b/.github/workflows/extension-libraries-upstream.yml @@ -0,0 +1,97 @@ +name: Extension libraries upstream + +# Advisory: does the extension-libraries upstream's ref still run under +# OpenSysML? It syncs the ref into the vendored directory and runs the whole +# suite. It is scheduled rather than a required check: it downloads from +# upstream and takes minutes, so a failure here surfaces what broke without +# blocking pull requests. It never pushes and never opens issues. +on: + schedule: + - cron: '41 4 * * *' + workflow_dispatch: + inputs: + ref: + description: 'Extension-libraries ref to sync (branch or tag)' + required: true + default: main + +permissions: + contents: read + +jobs: + upstream: + name: OpenSysML against upstream extension libraries + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + REF: ${{ inputs.ref || 'main' }} + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Install Z3 + run: | + sudo apt-get update + sudo apt-get install -y z3 + z3 --version + + - name: Install cvc5 + run: | + curl -fsSL --proto '=https' --proto-redir '=https' -o /tmp/cvc5.zip \ + https://github.com/cvc5/cvc5/releases/download/cvc5-1.3.4/cvc5-Linux-x86_64-static.zip + echo "dcdbfada0ce493ee98259c0816e0daafc561c223aadb3af298c2968e73ea39c6 /tmp/cvc5.zip" | sha256sum --check --strict + unzip -p /tmp/cvc5.zip '*/bin/cvc5' > /tmp/cvc5-bin + sudo install -m 0755 /tmp/cvc5-bin /usr/local/bin/cvc5 + cvc5 --version | head -1 + + - name: Sync the upstream ref + id: sync + run: | + out="$(scripts/sync-extension-libraries.sh --ref "$REF" | tee /dev/stderr)" + sha="$(sed -n 's/^Resolved .* to \([0-9a-f]*\)$/\1/p' <<<"$out" | tail -1)" + { + echo "### Synced upstream \`$REF\` at \`$sha\`" + echo '```' + git status --porcelain -- "internal/workspace/libs/stdlib/OpenSysML Libraries/" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Check the library snapshot + id: snapshot + run: | + if make stdlib-snapshot-check; then + echo "Snapshot unchanged." >> "$GITHUB_STEP_SUMMARY" + else + echo "Snapshot regenerated for the synced libraries." >> "$GITHUB_STEP_SUMMARY" + make stdlib-snapshot + fi + + - name: Run the test suite + id: tests + run: | + go test ./... + go test -C tools ./... + + - name: Report the verdict + if: always() + run: | + if [ "${{ steps.tests.outcome }}" = "success" ]; then + echo "Upstream \`$REF\` runs clean under OpenSysML." >> "$GITHUB_STEP_SUMMARY" + else + { + echo "**Upstream \`$REF\` breaks OpenSysML.**" + echo "" + echo "| step | outcome |" + echo "| --- | --- |" + echo "| sync | ${{ steps.sync.outcome }} |" + echo "| snapshot | ${{ steps.snapshot.outcome }} |" + echo "| tests | ${{ steps.tests.outcome }} |" + } >> "$GITHUB_STEP_SUMMARY" + fi + git checkout -- "internal/workspace/libs/stdlib/OpenSysML Libraries/" 2>/dev/null || true diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0146caa2b0..f6ad439d01 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -72,6 +72,12 @@ jobs: BASE_REF: ${{ github.base_ref }} run: scripts/ci-changed-areas.sh "origin/$BASE_REF" HEAD | tee -a "$GITHUB_OUTPUT" + # The vendored copy is checked in this job, not static-and-integrity: the + # changed-areas filter may skip that job for a non-Go change, but drift + # between the vendored libraries and their pin must always fail. + - name: Check the vendored extension libraries against their pin + run: make extension-libraries-check + race-tests: name: Go race tests (${{ matrix.shard }}) needs: changes diff --git a/Makefile b/Makefile index b1744d978b..a11db849a9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build build-sysml build-prod build-wasm-prod build-lsp build-grpc build-engine build-core build-syntax build-sysml-wasm build-release-wasm build-wasm build-wasm-wasip1 build-wasm-js wasm-check static-check windows-versioninfo-check man man-check install-tree pgo-profile conformance conformance-pkg conformance-rust conformance-julia conformance-matlab test test-shard coverage lint clean install help ontology-table ontology-table-check python-metamodel python-metamodel-check fuml-expected python-test python-coverage scripts-coverage node-coverage python-install proto proto-buf python-proto proto-ts proto-rust proto-lint proto-breaking vscode-grammar vscode-build vscode-package docs docs-install docs-serve docs-engine-assets docs-landing-assets docs-counts docs-check changelog-check changelog-render self-model +.PHONY: all build extension-libraries-check extension-libraries-sync build-sysml build-prod build-wasm-prod build-lsp build-grpc build-engine build-core build-syntax build-sysml-wasm build-release-wasm build-wasm build-wasm-wasip1 build-wasm-js wasm-check static-check windows-versioninfo-check man man-check install-tree pgo-profile conformance conformance-pkg conformance-rust conformance-julia conformance-matlab test test-shard coverage lint clean install help ontology-table ontology-table-check python-metamodel python-metamodel-check fuml-expected python-test python-coverage scripts-coverage node-coverage python-install proto proto-buf python-proto proto-ts proto-rust proto-lint proto-breaking vscode-grammar vscode-build vscode-package docs docs-install docs-serve docs-engine-assets docs-landing-assets docs-counts docs-check changelog-check changelog-render self-model # Version information # Only release tags describe a build; the moving `nightly` tag is not a version. @@ -315,6 +315,12 @@ test-short: ## Run Go tests without race detection go test -v ./... go test -C $(TOOLS_DIR) -v ./... +extension-libraries-check: ## Check the vendored extension libraries match their upstream pin + ./scripts/sync-extension-libraries.sh --check + +extension-libraries-sync: ## Re-vendor the extension libraries from the pinned upstream commit + ./scripts/sync-extension-libraries.sh + stdlib-snapshot: ## Regenerate the embedded snapshot of the bundled library after editing $(LIBS_DIR)/stdlib go generate ./$(LIBS_DIR) diff --git a/README.md b/README.md index 7e463b713a..1335c5ba6a 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ github.com/Open-MBEE/OpenSysML - **Parser:** hand-written recursive descent (no framework overhead, full error recovery, sub-millisecond parses) - **Grammar source:** OMG pilot Xtext grammars (`SysML.xtext` and `KerMLExpressions`) - **Spec compliance:** [OMG SysML v2.1 Beta 1 / KerML 1.1](https://www.omg.org/spec/SysML/2.0) (2026-08 release) -- **Standard library:** 94 files from [SysML v2 Pilot Implementation 2026-08](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/releases/tag/2026-08), byte-identical, plus the eleven non-normative `OpenSysML Libraries/` extensions (`OpenSysMLMathFunctions`, `DocumentQueries`, `IdentityMetadata`, `DiagramLayout`, [`OOSEM`](docs/project/oosem-library.md), [`MOSA`](docs/project/mosa-library.md), `StateSpaceIntegration`, `Stochastic`, `RandomFunctions`, `Simulation` and `MigrationMetadata`) +- **Standard library:** 94 files from [SysML v2 Pilot Implementation 2026-08](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/releases/tag/2026-08), byte-identical, plus the non-normative `OpenSysML Libraries/` extensions (`OpenSysMLMathFunctions`, `DocumentQueries`, `IdentityMetadata`, `DiagramLayout`, [`OOSEM`](docs/project/oosem-library.md), [`MOSA`](docs/project/mosa-library.md), `StateSpaceIntegration`, `Stochastic`, `RandomFunctions`, `Simulation`, `AnalysisRecords`, `StateMachines`, `SysMLValidation` and `MigrationMetadata`). The extensions are maintained upstream at [Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library) — for any SysML v2 tool — and vendored here pinned by `scripts/extension-libraries-pin.sh` ([pin, sync and contract](docs/project/extension-libraries.md)) - **CI/CD:** GitHub Actions checks pull requests; CircleCI builds and tests `main` and `develop` and publishes releases from tags ## Releases diff --git a/changes/unreleased/extension-libraries-upstream.changed.md b/changes/unreleased/extension-libraries-upstream.changed.md new file mode 100644 index 0000000000..d32c603adc --- /dev/null +++ b/changes/unreleased/extension-libraries-upstream.changed.md @@ -0,0 +1 @@ +- The non-normative `OpenSysML Libraries/` extension libraries are now maintained upstream at [Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library); the bundled copy is pinned by `scripts/extension-libraries-pin.sh`, refreshed by `scripts/sync-extension-libraries.sh` (`make extension-libraries-sync`), checked for drift on every pull request (`make extension-libraries-check`), and carries `engine-contract.json` recording the qualified names the engine binds. diff --git a/docs/guide/04-repl.md b/docs/guide/04-repl.md index 22f15972f7..4c52c3ba55 100644 --- a/docs/guide/04-repl.md +++ b/docs/guide/04-repl.md @@ -214,7 +214,10 @@ specialized view is marked `(from )`. A concern the viewpoint frames but t not is reported as `violated`. A concern that states no condition, or names one that does not resolve, is reported as `unevaluable` with the reason, not as a pass. These verdicts, and the way a nested view's framing is treated, are this implementation's own decisions, because -SysML v2 leaves verification verdict semantics non-normative. +SysML v2 leaves verification verdict semantics non-normative. (The extension +libraries these documents name are maintained upstream at +[Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library) +and vendored here pinned.) ``` sysml> package Demo { diff --git a/docs/guide/06-behavior.md b/docs/guide/06-behavior.md index 2c4fefe5b3..13eb96a0b3 100644 --- a/docs/guide/06-behavior.md +++ b/docs/guide/06-behavior.md @@ -993,7 +993,10 @@ in ten* — and by a duration — *the settle takes between one and eighty secon has no notation for either. OpenSysML supplies one as an extension library, in standard SysML v2 that any other tool reads as ordinary metadata and a function call: two library packages, `Stochastic` and `RandomFunctions` under `internal/workspace/libs/stdlib/OpenSysML Libraries/`, both -marked NON-NORMATIVE, with the vendored OMG files untouched. What they state is *modeled* +marked NON-NORMATIVE, with the vendored OMG files untouched. The extension libraries are +maintained upstream at +[Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library) +and vendored here pinned. What they state is *modeled* randomness, and the runtime keeps it apart from the scheduling kind: a weighted branch is drawn by its weights, a scheduling choice never is, and the two draw from two independent streams, fixed by two independent seeds. diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index dbe997eb48..03950dd796 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -664,7 +664,7 @@ See [the guide](../guide/) for VS Code configuration. | Standard library bundling | ✅ Complete | | LSP server implementation | ✅ Complete | -**Parser coverage:** 106/106 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the 12 non-normative OpenSysML extensions: `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `DocumentQueries.sysml`, `IdentityMetadata.sysml`, `OOSEM.sysml`, `DiagramLayout.sysml`, `MOSA.sysml`, `RandomFunctions.kerml`, `Simulation.sysml`, `StateSpaceIntegration.sysml`, `Stochastic.sysml`, `AnalysisRecords.sysml` and `MigrationMetadata.sysml`. Conformance verified by [stdlib_conformance_test.go](../../internal/workspace/libs/stdlib_conformance_test.go). Grammar reference available at [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). +**Parser coverage:** 106/106 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the 12 non-normative OpenSysML extensions: `OpenSysML Libraries/OpenSysMLMathFunctions.kerml`, `DocumentQueries.sysml`, `IdentityMetadata.sysml`, `OOSEM.sysml`, `DiagramLayout.sysml`, `MOSA.sysml`, `RandomFunctions.kerml`, `Simulation.sysml`, `StateSpaceIntegration.sysml`, `Stochastic.sysml`, `AnalysisRecords.sysml` and `MigrationMetadata.sysml`. The extensions are maintained upstream at [Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library) and vendored here pinned by `scripts/extension-libraries-pin.sh`; `engine-contract.json` beside them records the names the engine binds. Conformance verified by [stdlib_conformance_test.go](../../internal/workspace/libs/stdlib_conformance_test.go). Grammar reference available at [OMG Xtext grammar](https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation/tree/master/org.omg.kerml.xtext/src/org/omg/kerml/xtext). --- diff --git a/docs/manual/introduction.md b/docs/manual/introduction.md index 8155390038..5c388d82c6 100644 --- a/docs/manual/introduction.md +++ b/docs/manual/introduction.md @@ -46,7 +46,10 @@ model → queries → document plan → document tree → Markdown or HTML → ( Everything the engine understands is declared in one bundled library package, `DocumentQueries`. It is a non-normative OpenSysML extension — the types are ordinary SysML v2 declarations, so a model using them still parses everywhere, -but only OpenSysML gives them document semantics. +but only OpenSysML gives them document semantics. The extension libraries are +maintained upstream at +[Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library) +and vendored here pinned. **Query operations** (each a `calc def` taking and returning ordered element sequences): diff --git a/docs/manual/recording-analysis-runs.md b/docs/manual/recording-analysis-runs.md index e4b80a28b5..a6f0999c8c 100644 --- a/docs/manual/recording-analysis-runs.md +++ b/docs/manual/recording-analysis-runs.md @@ -169,7 +169,9 @@ calc def TimedRuns :> DocumentQueries::Query { ## The AnalysisRecords library `internal/workspace/libs/stdlib/OpenSysML Libraries/AnalysisRecords.sysml`, a -non-normative OpenSysML extension bundled like `DocumentQueries`, declares the +non-normative OpenSysML extension bundled like `DocumentQueries` (maintained upstream at +[Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library) +and vendored here pinned), declares the vocabulary the records are written in: - `RecordedRun` — the metadata annotation a record carries: `runAt` (the UTC diff --git a/docs/manual/troubleshooting.md b/docs/manual/troubleshooting.md index 30e85a20e2..baabea2d5b 100644 --- a/docs/manual/troubleshooting.md +++ b/docs/manual/troubleshooting.md @@ -113,7 +113,9 @@ position; they are tracked in the project's compliance record. - **The vocabulary is non-normative.** `DocumentQueries` is an OpenSysML extension; other SysML v2 tools will parse models that use it but will not - render documents from them. + render documents from them. The extension libraries are maintained upstream + at [Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library) + and vendored here pinned. - **Query-generated runs cannot cross-reference.** Column runs restyle query-produced text and can link to external URLs, but `Ref`-style cross-references to other content blocks apply to statically-authored runs diff --git a/docs/project/diagram-layout-annotations.md b/docs/project/diagram-layout-annotations.md index 8c67bc12ac..9e8135c88b 100644 --- a/docs/project/diagram-layout-annotations.md +++ b/docs/project/diagram-layout-annotations.md @@ -47,7 +47,8 @@ model changes: its renderings are byte-identical to what they were. Six metadata definitions, shipped as a non-normative OpenSysML library extension in the same tier as `IdentityMetadata` (`internal/workspace/libs/stdlib/OpenSysML Libraries/DiagramLayout.sysml`, counted by the stdlib conformance gate with the other -extensions): +extensions; maintained upstream at Open-MBEE/OpenSysML-Extensions-Library and vendored +here pinned by `scripts/extension-libraries-pin.sh`): ```sysml standard library package DiagramLayout { diff --git a/docs/project/element-identity-annotations.md b/docs/project/element-identity-annotations.md index b49d0e6b43..9be8162cc8 100644 --- a/docs/project/element-identity-annotations.md +++ b/docs/project/element-identity-annotations.md @@ -51,7 +51,9 @@ update of the same element rather than a delete and a create. ## The metadata library Two metadata definitions, shipped as a non-normative OpenSysML library extension (the -stdlib already carries two such files; the conformance gate counts them separately): +stdlib already carries two such files; the conformance gate counts them separately; +maintained upstream at Open-MBEE/OpenSysML-Extensions-Library and vendored here pinned +by `scripts/extension-libraries-pin.sh`): ```sysml standard library package IdentityMetadata { diff --git a/docs/project/extension-libraries.md b/docs/project/extension-libraries.md new file mode 100644 index 0000000000..85da53956a --- /dev/null +++ b/docs/project/extension-libraries.md @@ -0,0 +1,42 @@ +# Extension libraries — pin, sync and engine contract + +The non-normative OpenSysML extension libraries (`OpenSysMLMathFunctions`, +`DocumentQueries`, `IdentityMetadata`, `DiagramLayout`, `OOSEM`, `MOSA`, +`StateSpaceIntegration`, `Stochastic`, `RandomFunctions`, `Simulation`, +`AnalysisRecords`, `MigrationMetadata`, `StateMachines` and `SysMLValidation`) +are maintained upstream at +[Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library), +with their history imported from this repository. Other tools get them from +that repository or from the `.kpar` project archive each upstream release +attaches. + +OpenSysML consumes them as a vendored copy at +`internal/workspace/libs/stdlib/OpenSysML Libraries/`: + +- `scripts/extension-libraries-pin.sh` records the repository and the commit + the copy was taken from (`EXTENSIONS_REPO`, `EXTENSIONS_REF`, + `EXTENSIONS_COMMIT`, each environment-overridable). +- `scripts/sync-extension-libraries.sh` rewrites the copy from the pin. + `--check` reports drift without writing (run by `make + extension-libraries-check`, which CI runs on every pull request); `--ref + REF` syncs a branch or tag instead of the pin; `--source DIR` syncs a local + upstream checkout. An upstream holding no libraries or no + `engine-contract.json` is refused, and a file + the upstream does not carry — `README.md` aside, which the vendored copy + owns — is an error, so edits are never made here: they go upstream, the pin + is bumped, and the sync re-runs. +- `engine-contract.json`, synced beside the libraries, is the machine-read + contract: the qualified names the engine binds — the functions the runtime + registry implements, the metadata and features it reads, the document + elements the doc and query plans compile, and the names the SysML v1 + migrator writes. `internal/exec/runtime/engine_contract_test.go` holds the + registry to the manifest's `function` entries; + `tests/hygiene/engine_contract_test.go` derives every extension-qualified + name the code binds and requires it to be a manifest entry. Breaking the + contract upstream requires a `contract` bump, a coordinated OpenSysML pull + request that re-pins, and a MAJOR upstream release. +- `.github/workflows/extension-libraries-upstream.yml` is the advisory + nightly: it syncs the upstream `main` (or a dispatch-given ref), checks the + library snapshot and runs the whole suite in both Go modules (`./...` and + `tools`), so an upstream change that + breaks OpenSysML surfaces within a day. It is not a required check. diff --git a/docs/project/mosa-library.md b/docs/project/mosa-library.md index 9fd911f047..add0f45e00 100644 --- a/docs/project/mosa-library.md +++ b/docs/project/mosa-library.md @@ -1,6 +1,6 @@ # MOSA library — design -Status: **implemented.** `internal/workspace/libs/stdlib/OpenSysML Libraries/MOSA.sysml` is bundled +Status: **implemented.** `internal/workspace/libs/stdlib/OpenSysML Libraries/MOSA.sysml` — maintained upstream at Open-MBEE/OpenSysML-Extensions-Library and vendored here pinned by `scripts/extension-libraries-pin.sh` — is bundled with the other OpenSysML extensions, enters the same conformance, strict-notation and snapshot gates, and is exercised by `examples/mosa-demo/mosa-demo.sysml` (`TestExamplesAnalyseCleanly`). `passes.MOSAPass` audits models written against it. diff --git a/docs/project/oosem-library.md b/docs/project/oosem-library.md index f40a8ae5ea..5c12e0581a 100644 --- a/docs/project/oosem-library.md +++ b/docs/project/oosem-library.md @@ -1,8 +1,9 @@ # OOSEM library — design Status: **implemented.** `internal/workspace/libs/stdlib/OpenSysML Libraries/OOSEM.sysml` is bundled -with the other OpenSysML extensions, enters the same conformance, strict-notation and snapshot -gates, and is exercised by `examples/oosem-demo/oosem-demo.sysml` +with the other OpenSysML extensions — maintained upstream at Open-MBEE/OpenSysML-Extensions-Library +and vendored here pinned by `scripts/extension-libraries-pin.sh` — enters the same conformance, +strict-notation and snapshot gates, and is exercised by `examples/oosem-demo/oosem-demo.sysml` (`TestExamplesAnalyseCleanly`). Both the library and the example also validate cleanly under the pinned OMG pilot implementation, so the notation they use is standard SysML v2. diff --git a/docs/project/pilot-differential.md b/docs/project/pilot-differential.md index 1ff40b678b..2461166ea6 100644 --- a/docs/project/pilot-differential.md +++ b/docs/project/pilot-differential.md @@ -102,6 +102,9 @@ build/pilot-sysml-validator/validate-sysml-batch model.sysml \ "internal/workspace/libs/stdlib/OpenSysML Libraries" ``` +The extensions under it are maintained upstream at Open-MBEE/OpenSysML-Extensions-Library and +vendored here pinned by `scripts/extension-libraries-pin.sh`. + This harness still hands each language to its own bridge (a `.kerml` file of a root goes to `validate-kerml`), so the committed baseline's verdicts are unchanged by this; only the bridge's source digest in its provenance moved. diff --git a/docs/project/roadmap.md b/docs/project/roadmap.md index 7081dca6f8..9bbb928559 100644 --- a/docs/project/roadmap.md +++ b/docs/project/roadmap.md @@ -2361,7 +2361,7 @@ supplies it, and the seam is the one the runtime already has. ## B1 — the binding vocabulary in the notation -A `Bindings` package under `OpenSysML Libraries/`, beside `IdentityMetadata`, as standard +A `Bindings` package under `OpenSysML Libraries/` — the extension libraries now maintained upstream at [Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library) and vendored here pinned — beside `IdentityMetadata`, as standard user-defined metadata so every conforming tool reads a bound model as an annotated one: an `@External` on a calc or function definition whose body is absent (the host computes it), an `@ExternalValue` on an attribute or item usage with no value expression (the host supplies it, diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 775c1e4a39..dabf4f4baa 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -3136,7 +3136,8 @@ derivation links have no dedicated semantic representation yet and are unknown k **Standard:** none. This is the document layer of the OpenSysML document-query extension: a document authored natively in SysML v2 as a part definition specializing the bundled, non-normative -`DocumentQueries::Document`, compiled into an immutable document plan, and evaluated into an +`DocumentQueries::Document` (the extension libraries are maintained upstream at +Open-MBEE/OpenSysML-Extensions-Library and vendored here pinned), compiled into an immutable document plan, and evaluated into an immutable, backend-agnostic document tree by executing its queries through the execution engine above. It does not alter SysML v2 language semantics. diff --git a/docs/project/statemachines-library.md b/docs/project/statemachines-library.md index afd5d35a2d..b1393d0272 100644 --- a/docs/project/statemachines-library.md +++ b/docs/project/statemachines-library.md @@ -1,6 +1,6 @@ # StateMachines library — design -Status: **implemented.** `internal/workspace/libs/stdlib/OpenSysML Libraries/StateMachines.sysml` +Status: **implemented.** `internal/workspace/libs/stdlib/OpenSysML Libraries/StateMachines.sysml` — maintained upstream at Open-MBEE/OpenSysML-Extensions-Library and vendored here pinned by `scripts/extension-libraries-pin.sh` is bundled with the other OpenSysML libraries, enters the same conformance and snapshot gates, and the notation it enables is exercised end to end by the `*_metadata.sysml` conformance fixtures and by `examples/pseudostates-demo.sysml`. diff --git a/internal/exec/runtime/engine_contract_test.go b/internal/exec/runtime/engine_contract_test.go new file mode 100644 index 0000000000..dc87de1f16 --- /dev/null +++ b/internal/exec/runtime/engine_contract_test.go @@ -0,0 +1,84 @@ +package runtime + +import ( + "sort" + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" + "github.com/Open-MBEE/OpenSysML/tests/testutil/enginecontract" +) + +// The extension packages the vendored library carries and the runtime function +// registry cannot drift: every registry entry under an extension package is a +// manifest `function` entry with identical parameters in order, and every +// manifest `function` entry in a package the registry implements is registered. +func TestEngineContractMatchesTheRuntimeRegistry(t *testing.T) { + src := libs.DefaultSource() + manifest, err := enginecontract.Load(src) + if err != nil { + t.Fatalf("%v", err) + } + packages, err := enginecontract.Packages(src) + if err != nil { + t.Fatalf("%v", err) + } + byName := manifest.ByName() + + update := "update engine-contract.json upstream in Open-MBEE/OpenSysML-Extensions-Library, bump scripts/extension-libraries-pin.sh, and run scripts/sync-extension-libraries.sh" + + registryPackages := map[string]bool{} + for name := range libraryFunctions { + pkg, _, _ := strings.Cut(name, "::") + registryPackages[pkg] = true + } + + for name, fn := range libraryFunctions { + pkg, _, _ := strings.Cut(name, "::") + extension := sort.SearchStrings(packages, pkg) < len(packages) && packages[sort.SearchStrings(packages, pkg)] == pkg + if !extension { + continue + } + entry, ok := byName[name] + if !ok { + t.Errorf("OpenSysML binds %s in the runtime registry but engine-contract.json does not list it; %s", name, update) + continue + } + if entry.Kind != enginecontract.KindFunction { + t.Errorf("%s is a registry function but the manifest kind is %q; %s", name, entry.Kind, update) + continue + } + registryParams := make([]string, len(fn.params)) + for i, p := range fn.params { + registryParams[i] = p.name + } + if !equalParamNames(entry.Parameters, registryParams) { + t.Errorf("%s parameters: registry %v != manifest %v; %s", name, registryParams, entry.Parameters, update) + } + } + + for _, entry := range manifest.Entries { + if entry.Kind != enginecontract.KindFunction { + continue + } + pkg, _, _ := strings.Cut(entry.Name, "::") + if !registryPackages[pkg] { + continue + } + if _, ok := libraryFunctions[entry.Name]; !ok { + t.Errorf("engine-contract.json lists function %s but the runtime registry does not implement it; %s", entry.Name, update) + } + } +} + +func equalParamNames(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/README.md b/internal/workspace/libs/stdlib/OpenSysML Libraries/README.md new file mode 100644 index 0000000000..5735646c55 --- /dev/null +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/README.md @@ -0,0 +1,12 @@ +# OpenSysML extension libraries (vendored) + +The libraries in this directory are maintained upstream at +[Open-MBEE/OpenSysML-Extensions-Library](https://github.com/Open-MBEE/OpenSysML-Extensions-Library). +This copy is pinned by `scripts/extension-libraries-pin.sh` and written by +`scripts/sync-extension-libraries.sh`. + +Do not edit these files here: change them upstream, bump the pin, and sync. +CI fails when this copy drifts from the pinned upstream. + +`engine-contract.json` lists the qualified names in these libraries that the +engine binds to; the contract is checked here and upstream. diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json b/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json new file mode 100644 index 0000000000..372004f4be --- /dev/null +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json @@ -0,0 +1,1045 @@ +{ + "contract": 1, + "description": "Qualified names in libraries/ that Open-MBEE/OpenSysML binds to. Removing, renaming or changing an entry is a breaking change.", + "entries": [ + { + "name": "AnalysisRecords::AnalysisRun", + "kind": "element", + "consumers": [ + "internal/exec/analysis/record/record.go", + "internal/frontend/repl/record.go" + ] + }, + { + "name": "AnalysisRecords::EvaluationRecord", + "kind": "element", + "consumers": [ + "internal/exec/analysis/record/record.go" + ] + }, + { + "name": "AnalysisRecords::RecordedRun", + "kind": "metadata", + "attributes": [ + "runAt", + "tool", + "command", + "kind", + "tools" + ], + "consumers": [ + "internal/exec/analysis/record/record.go" + ] + }, + { + "name": "AnalysisRecords::VerdictRecord", + "kind": "element", + "consumers": [ + "internal/exec/analysis/record/record.go" + ] + }, + { + "name": "DiagramLayout", + "kind": "package", + "consumers": [] + }, + { + "name": "DiagramLayout::Canvas", + "kind": "metadata", + "attributes": [ + "unit", + "width", + "height" + ], + "migratorEmitted": true, + "consumers": [ + "internal/ir/view/geometry.go", + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Canvas::height", + "kind": "element", + "consumers": [ + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Layout", + "kind": "metadata", + "attributes": [ + "x", + "y", + "width", + "height", + "collapsed" + ], + "migratorEmitted": true, + "consumers": [ + "internal/ir/view/geometry.go", + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Layout::height", + "kind": "element", + "consumers": [ + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Note", + "kind": "metadata", + "attributes": [ + "text", + "x", + "y", + "width", + "height" + ], + "migratorEmitted": true, + "consumers": [ + "internal/ir/view/geometry.go", + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Note::height", + "kind": "element", + "consumers": [ + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Picture", + "kind": "metadata", + "attributes": [ + "location", + "x", + "y", + "width", + "height", + "alt", + "above" + ], + "migratorEmitted": true, + "consumers": [ + "internal/ir/view/geometry.go", + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Picture::width", + "kind": "element", + "consumers": [ + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Route", + "kind": "metadata", + "attributes": [ + "points" + ], + "migratorEmitted": true, + "consumers": [ + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Route::points", + "kind": "element", + "consumers": [ + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Style", + "kind": "metadata", + "attributes": [ + "fill", + "line", + "text", + "font", + "fontSize", + "bold", + "italic" + ], + "migratorEmitted": true, + "consumers": [ + "internal/ir/view/geometry.go", + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DiagramLayout::Style::text", + "kind": "element", + "consumers": [ + "internal/semantic/semantics/layout.go" + ] + }, + { + "name": "DocumentQueries", + "kind": "package", + "consumers": [] + }, + { + "name": "DocumentQueries::Ancestors", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Column", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/columns.go", + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::ColumnRun", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::ColumnSpec", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/columns.go" + ] + }, + { + "name": "DocumentQueries::Definitions", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Descendants", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Diagram", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Distinct", + "kind": "document", + "migratorEmitted": true, + "consumers": [ + "internal/ir/queryplan/navigation.go", + "internal/translate/migrate/ocllower.go" + ] + }, + { + "name": "DocumentQueries::Document", + "kind": "document", + "consumers": [ + "internal/frontend/grpc/docquery.go", + "internal/frontend/repl/docrender.go" + ] + }, + { + "name": "DocumentQueries::Event", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go", + "internal/doc/queryexec/events.go" + ] + }, + { + "name": "DocumentQueries::Events", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Except", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Formula", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Image", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go", + "internal/doc/docrender/artwork.go" + ] + }, + { + "name": "DocumentQueries::InState", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Link", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::LinkColumn", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::List", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Named", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Objects", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::OrderBy", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::OwnedElements", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Paragraph", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Project", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Query", + "kind": "document", + "consumers": [ + "internal/frontend/grpc/docquery.go", + "internal/frontend/repl/docquery.go" + ] + }, + { + "name": "DocumentQueries::Ref", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::RelatedColumn", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/columns.go" + ] + }, + { + "name": "DocumentQueries::RelatedElements", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Run", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Section", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Span", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::SpanColumn", + "kind": "document", + "consumers": [ + "internal/ir/docplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::State", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go", + "internal/doc/queryexec/states.go" + ] + }, + { + "name": "DocumentQueries::States", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Table", + "kind": "document", + "migratorEmitted": true, + "consumers": [ + "internal/ir/docplan/compiler.go", + "internal/translate/migrate/tables.go" + ] + }, + { + "name": "DocumentQueries::Tree", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Union", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::Verdict", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go", + "internal/doc/queryexec/verdicts.go" + ] + }, + { + "name": "DocumentQueries::Verdicts", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::WhereFeature", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::WhereMetadata", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::WhereName", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::WhereRelated", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::WhereText", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "DocumentQueries::WhereType", + "kind": "document", + "consumers": [ + "internal/ir/queryplan/compiler.go" + ] + }, + { + "name": "IdentityMetadata::ElementId", + "kind": "metadata", + "attributes": [ + "id" + ], + "consumers": [ + "internal/semantic/identity/identity.go", + "internal/translate/export/rdf_in.go" + ] + }, + { + "name": "IdentityMetadata::ProjectRef", + "kind": "metadata", + "attributes": [ + "projectId", + "branch", + "org" + ], + "consumers": [ + "internal/semantic/identity/identity.go", + "internal/translate/export/rdf_in.go" + ] + }, + { + "name": "MOSA::ConformantMetadata", + "kind": "metadata", + "attributes": [], + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::ConformedStandardMetadata", + "kind": "metadata", + "attributes": [], + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::DataRights", + "kind": "metadata", + "attributes": [], + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::InterfaceControl", + "kind": "metadata", + "attributes": [ + "authority" + ], + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::InterfaceRequirement", + "kind": "element", + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::MajorSystemComponent", + "kind": "element", + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::MajorSystemPlatform", + "kind": "element", + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::ModularSystem", + "kind": "element", + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::ModularSystemInterface", + "kind": "element", + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::Proprietary", + "kind": "metadata", + "attributes": [ + "rationale" + ], + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::Standard", + "kind": "element", + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MOSA::StandardConformanceMetadata", + "kind": "metadata", + "attributes": [], + "consumers": [ + "internal/check/passes/mosa.go" + ] + }, + { + "name": "MigrationMetadata", + "kind": "package", + "consumers": [] + }, + { + "name": "MigrationMetadata::DeferredEvent", + "kind": "metadata", + "attributes": [ + "signal" + ], + "migratorEmitted": true, + "consumers": [ + "internal/ir/lower/state_metadata.go", + "internal/check/passes/lint_deferred_keeper.go" + ] + }, + { + "name": "MigrationMetadata::DeferredKeeper", + "kind": "metadata", + "attributes": [], + "migratorEmitted": true, + "consumers": [ + "internal/exec/runtime/action_choice.go", + "internal/ir/lower/action_graph.go" + ] + }, + { + "name": "MigrationMetadata::LibraryNameAvoided", + "kind": "metadata", + "attributes": [ + "sourceName" + ], + "migratorEmitted": true, + "consumers": [ + "internal/translate/migrate/migrate.go" + ] + }, + { + "name": "MigrationMetadata::StandIn", + "kind": "metadata", + "attributes": [], + "migratorEmitted": true, + "consumers": [ + "internal/semantic/semantics/synthesized_name.go", + "internal/translate/migrate/names.go" + ] + }, + { + "name": "MigrationMetadata::SynthesizedName", + "kind": "metadata", + "attributes": [], + "migratorEmitted": true, + "consumers": [ + "internal/semantic/semantics/synthesized_name.go", + "internal/translate/migrate/names.go" + ] + }, + { + "name": "OOSEM::ComponentRequirement", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::DataComponent", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::Enterprise", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::EnterpriseUseCase", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::LogicalComponent", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::MissionRequirement", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::Node", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::PhysicalComponent", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::StakeholderNeed", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::SystemContext", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::SystemRequirement", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OOSEM::SystemUseCase", + "kind": "element", + "consumers": [ + "internal/check/passes/oosem_method.go" + ] + }, + { + "name": "OpenSysMLMathFunctions", + "kind": "package", + "consumers": [] + }, + { + "name": "OpenSysMLMathFunctions::atan2", + "kind": "function", + "parameters": [ + "y", + "x" + ], + "consumers": [ + "internal/exec/runtime/library_functions.go" + ] + }, + { + "name": "OpenSysMLMathFunctions::ceiling", + "kind": "function", + "parameters": [ + "x" + ], + "migratorEmitted": true, + "consumers": [ + "internal/exec/runtime/library_functions.go", + "internal/translate/migrate/opaque.go" + ] + }, + { + "name": "OpenSysMLMathFunctions::exp", + "kind": "function", + "parameters": [ + "x" + ], + "consumers": [ + "internal/exec/runtime/library_functions.go" + ] + }, + { + "name": "OpenSysMLMathFunctions::ln", + "kind": "function", + "parameters": [ + "x" + ], + "consumers": [ + "internal/exec/runtime/library_functions.go" + ] + }, + { + "name": "OpenSysMLMathFunctions::log", + "kind": "function", + "parameters": [ + "x", + "base" + ], + "consumers": [ + "internal/exec/runtime/library_functions.go" + ] + }, + { + "name": "OpenSysMLMathFunctions::quotient", + "kind": "function", + "parameters": [ + "x", + "y" + ], + "migratorEmitted": true, + "consumers": [ + "internal/exec/runtime/library_functions.go", + "internal/translate/migrate/opaque.go" + ] + }, + { + "name": "RandomFunctions", + "kind": "package", + "consumers": [] + }, + { + "name": "RandomFunctions::normal", + "kind": "function", + "parameters": [ + "mean", + "sd" + ], + "consumers": [ + "internal/exec/runtime/random_functions.go" + ] + }, + { + "name": "RandomFunctions::triangular", + "kind": "function", + "parameters": [ + "lo", + "mode", + "hi" + ], + "consumers": [ + "internal/exec/runtime/random_functions.go" + ] + }, + { + "name": "RandomFunctions::uniform", + "kind": "function", + "parameters": [ + "lo", + "hi" + ], + "migratorEmitted": true, + "consumers": [ + "internal/exec/runtime/random_functions.go", + "internal/translate/migrate/activity.go" + ] + }, + { + "name": "RandomFunctions::uniformInteger", + "kind": "function", + "parameters": [ + "lo", + "hi" + ], + "consumers": [ + "internal/exec/runtime/random_functions.go" + ] + }, + { + "name": "Simulation::Configuration", + "kind": "metadata", + "attributes": [ + "runs", + "draws", + "timeVariable", + "startTime", + "stepSize", + "timeUnit", + "parallelForks" + ], + "migratorEmitted": true, + "consumers": [ + "internal/translate/migrate/simconfig.go" + ] + }, + { + "name": "Simulation::DrawPolicy", + "kind": "element", + "migratorEmitted": true, + "consumers": [ + "internal/translate/migrate/simconfig.go" + ] + }, + { + "name": "Simulation::MonteCarlo", + "kind": "element", + "migratorEmitted": true, + "consumers": [ + "internal/exec/runtime/montecarlo_case.go", + "internal/exec/simresults/simresults.go" + ] + }, + { + "name": "StateMachines", + "kind": "package", + "consumers": [] + }, + { + "name": "StateMachines::ChoiceMetadata", + "kind": "metadata", + "attributes": [], + "consumers": [ + "internal/ir/lower/state_metadata.go" + ] + }, + { + "name": "StateMachines::DeepHistoryMetadata", + "kind": "metadata", + "attributes": [], + "consumers": [ + "internal/ir/lower/state_metadata.go" + ] + }, + { + "name": "StateMachines::JunctionMetadata", + "kind": "metadata", + "attributes": [], + "consumers": [ + "internal/ir/lower/state_metadata.go" + ] + }, + { + "name": "StateMachines::ShallowHistoryMetadata", + "kind": "metadata", + "attributes": [], + "consumers": [ + "internal/ir/lower/state_metadata.go" + ] + }, + { + "name": "StateMachines::choice", + "kind": "element", + "consumers": [ + "internal/check/passes/nonstandard_notation.go", + "internal/translate/migrate/states.go" + ] + }, + { + "name": "StateMachines::deepHistory", + "kind": "element", + "migratorEmitted": true, + "consumers": [ + "internal/translate/migrate/states.go" + ] + }, + { + "name": "StateMachines::junction", + "kind": "element", + "migratorEmitted": true, + "consumers": [ + "internal/translate/migrate/states.go" + ] + }, + { + "name": "StateMachines::shallowHistory", + "kind": "element", + "migratorEmitted": true, + "consumers": [ + "internal/translate/migrate/states.go" + ] + }, + { + "name": "StateSpaceIntegration::Euler", + "kind": "element", + "consumers": [ + "internal/ir/lower/state_space.go" + ] + }, + { + "name": "StateSpaceIntegration::FixedStepDynamics", + "kind": "element", + "consumers": [ + "internal/ir/lower/state_space.go" + ] + }, + { + "name": "StateSpaceIntegration::RK4", + "kind": "element", + "consumers": [ + "internal/ir/lower/state_space.go" + ] + }, + { + "name": "Stochastic::Probability", + "kind": "metadata", + "attributes": [ + "p" + ], + "migratorEmitted": true, + "consumers": [ + "internal/semantic/semantics/stochastic.go", + "internal/translate/migrate/activity.go" + ] + }, + { + "name": "Stochastic::Probability::p", + "kind": "element", + "consumers": [ + "internal/semantic/semantics/stochastic.go" + ] + }, + { + "name": "SysMLValidation", + "kind": "package", + "consumers": [] + } + ] +} diff --git a/mkdocs.yml b/mkdocs.yml index 8af28230d5..73a0659a2f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -204,6 +204,7 @@ exclude_docs: | project/oosem-library.md project/mosa-library.md project/statemachines-library.md + project/extension-libraries.md nav: - Home: README.md diff --git a/scripts/extension-libraries-pin.sh b/scripts/extension-libraries-pin.sh new file mode 100755 index 0000000000..d3b37dac87 --- /dev/null +++ b/scripts/extension-libraries-pin.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Single source of the extension-libraries pin: the OpenSysML-Extensions-Library +# repository, the commit scripts/sync-extension-libraries.sh vendors +# "OpenSysML Libraries/" from, and the ref that commit came from. +# +# Kept in one file so what CI checks and what a maintainer syncs cannot drift. +# The ref names where the pin was taken; the commit is what every fetch +# verifies, because a ref is mutable. Change them together. +EXTENSIONS_REPO="${EXTENSIONS_REPO:-https://github.com/Open-MBEE/OpenSysML-Extensions-Library.git}" +EXTENSIONS_REF="${EXTENSIONS_REF:-main}" +EXTENSIONS_COMMIT="${EXTENSIONS_COMMIT:-4213d77dc89ad89e79591e425536d6ef9e9dab89}" diff --git a/scripts/sync-extension-libraries.sh b/scripts/sync-extension-libraries.sh new file mode 100755 index 0000000000..76202eccd7 --- /dev/null +++ b/scripts/sync-extension-libraries.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Sync the vendored extension libraries from Open-MBEE/OpenSysML-Extensions-Library. +# +# Upstream set: libraries/*.sysml, libraries/*.kerml and engine-contract.json, +# written flat into internal/workspace/libs/stdlib/OpenSysML Libraries/. That +# directory owns README.md only; any other file that is not upstream is an +# error, so hand edits there fail loudly instead of silently surviving a sync. +# +# scripts/sync-extension-libraries.sh vendor EXTENSIONS_COMMIT +# scripts/sync-extension-libraries.sh --ref REF vendor a branch or tag +# scripts/sync-extension-libraries.sh --source DIR vendor a local tree +# scripts/sync-extension-libraries.sh --check diff only; exit 1 on drift +set -euo pipefail + +# shellcheck source=scripts/extension-libraries-pin.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/extension-libraries-pin.sh" + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +vendored="$repo_root/internal/workspace/libs/stdlib/OpenSysML Libraries" + +check=0 +ref="" +source_dir="" +while [[ $# -gt 0 ]]; do + case "$1" in + --check) + check=1 + shift + ;; + --ref) + ref="${2:?--ref needs a branch or tag}" + shift 2 + ;; + --source) + source_dir="${2:?--source needs a directory}" + shift 2 + ;; + *) + echo "error: unknown argument: $1" >&2 + exit 2 + ;; + esac +done +if [[ -n "$ref" && -n "$source_dir" ]]; then + echo "error: --ref and --source are exclusive" >&2 + exit 2 +fi + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +upstream="$work/upstream" +mkdir -p "$upstream" + +if [[ -n "$source_dir" ]]; then + if [[ ! -d "$source_dir/libraries" ]]; then + echo "error: $source_dir has no libraries/ directory" >&2 + exit 1 + fi + cp "$source_dir"/libraries/*.sysml "$source_dir"/libraries/*.kerml "$upstream/" 2>/dev/null || true + if [[ -f "$source_dir/engine-contract.json" ]]; then + cp "$source_dir/engine-contract.json" "$upstream/" + fi + resolved="local tree $source_dir" +else + fetch_ref="$EXTENSIONS_COMMIT" + [[ -n "$ref" ]] && fetch_ref="$ref" + git init --quiet "$work/git" + git -C "$work/git" remote add origin "$EXTENSIONS_REPO" + echo "Fetching $fetch_ref from $EXTENSIONS_REPO ..." + if ! git -C "$work/git" fetch --quiet --depth 1 origin "$fetch_ref"; then + echo "error: could not fetch $fetch_ref from $EXTENSIONS_REPO" >&2 + exit 1 + fi + resolved="$(git -C "$work/git" rev-parse FETCH_HEAD)" + if [[ -z "$ref" ]] && [[ "$resolved" != "$EXTENSIONS_COMMIT" ]]; then + echo "error: fetched $resolved, scripts/extension-libraries-pin.sh pins $EXTENSIONS_COMMIT" >&2 + exit 1 + fi + echo "Resolved $fetch_ref to $resolved" + # The fetch has no checkout: stream each upstream file out of the object store. + while IFS= read -r path; do + mkdir -p "$work/tree/$(dirname "$path")" + git -C "$work/git" show "FETCH_HEAD:$path" >"$work/tree/$path" + done < <(git -C "$work/git" ls-tree -r --name-only FETCH_HEAD -- libraries engine-contract.json) + cp "$work/tree"/libraries/*.sysml "$work/tree"/libraries/*.kerml "$upstream/" 2>/dev/null || true + if [[ -f "$work/tree/engine-contract.json" ]]; then + cp "$work/tree/engine-contract.json" "$upstream/" + fi +fi + +# An upstream holding no libraries at all is refused: it would empty the +# vendored directory, which is how a sync of a ref that does not carry them +# (such as a branch cut before the import) would silently break the engine. +upstream_libs="$(find "$upstream" -maxdepth 1 -type f \( -name '*.sysml' -o -name '*.kerml' \) | wc -l | tr -d ' ')" +if [[ "$upstream_libs" -eq 0 ]]; then + echo "error: upstream holds no .sysml or .kerml library file; refusing to sync an empty set" >&2 + exit 1 +fi + +# Without the contract the sync would delete the vendored manifest as drift. +if [[ ! -f "$upstream/engine-contract.json" ]]; then + echo "error: upstream holds no engine-contract.json; refusing to sync without the engine contract" >&2 + exit 1 +fi + +added=() +removed=() +changed=() +unexpected=() + +for file in "$upstream"/*; do + name="$(basename "$file")" + if [[ ! -f "$vendored/$name" ]]; then + added+=("$name") + elif ! cmp -s "$file" "$vendored/$name"; then + changed+=("$name") + fi +done +for file in "$vendored"/*.sysml "$vendored"/*.kerml "$vendored/engine-contract.json"; do + [[ -e "$file" ]] || continue + name="$(basename "$file")" + [[ -f "$upstream/$name" ]] || removed+=("$name") +done +# Anything else in the vendored directory that is neither upstream nor the +# OpenSysML-owned README.md is unexpected. +for file in "$vendored"/*; do + name="$(basename "$file")" + [[ "$name" == "README.md" ]] && continue + [[ -f "$upstream/$name" ]] && continue + case "$name" in + *.sysml | *.kerml | engine-contract.json) ;; # already in removed[] + *) unexpected+=("$name") ;; + esac +done + +if [[ "${#unexpected[@]}" -gt 0 ]]; then + printf 'error: unexpected file(s) in %s: %s\n' "$vendored" "${unexpected[*]}" >&2 + echo " that directory is written by this script; remove them or move them upstream" >&2 + exit 1 +fi + +drift=$(( ${#added[@]} + ${#removed[@]} + ${#changed[@]} )) +if [[ "$drift" -eq 0 ]]; then + echo "Extension libraries in sync with $resolved" + exit 0 +fi + +for name in "${added[@]}"; do echo " added $name"; done +for name in "${changed[@]}"; do echo " changed $name"; done +for name in "${removed[@]}"; do echo " removed $name"; done + +if [[ "$check" -eq 1 ]]; then + echo "error: the vendored extension libraries differ from $resolved" >&2 + echo " edits go upstream to Open-MBEE/OpenSysML-Extensions-Library;" >&2 + echo " bump scripts/extension-libraries-pin.sh and run scripts/sync-extension-libraries.sh" >&2 + exit 1 +fi + +for name in "${removed[@]}"; do rm -f "$vendored/$name"; done +for name in "${added[@]}" "${changed[@]}"; do cp "$upstream/$name" "$vendored/$name"; done +echo "Synced extension libraries to $resolved" diff --git a/tests/hygiene/engine_contract_test.go b/tests/hygiene/engine_contract_test.go new file mode 100644 index 0000000000..66c1c0b4da --- /dev/null +++ b/tests/hygiene/engine_contract_test.go @@ -0,0 +1,231 @@ +package hygiene + +import ( + "go/ast" + "go/parser" + "go/token" + "io/fs" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" + "github.com/Open-MBEE/OpenSysML/tests/testutil/enginecontract" +) + +// engineContractPkgs loads the extension package set from the vendored +// library: every file stem under the vendored directory names one package. +func engineContractPkgs(t *testing.T) []string { + t.Helper() + pkgs, err := enginecontract.Packages(libs.DefaultSource()) + if err != nil { + t.Fatalf("%v", err) + } + return pkgs +} + +// stringConsts collects the package-level string constants of every file so +// identifiers naming them fold too: `"Pkg::" + suffix` folds when Pkg's +// prefix is a const declared beside it. +func stringConsts(files map[string][]*ast.File) map[*ast.File]map[string]string { + out := map[*ast.File]map[string]string{} + for _, fset := range files { + consts := map[string]string{} + for _, file := range fset { + for _, decl := range file.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || gen.Tok != token.CONST { + continue + } + for _, spec := range gen.Specs { + vs, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for i, name := range vs.Names { + if i < len(vs.Values) { + if lit, ok := vs.Values[i].(*ast.BasicLit); ok && lit.Kind == token.STRING { + consts[name.Name] = unquote(lit.Value) + } + } + } + } + } + out[file] = consts + } + } + return out +} + +func unquote(lit string) string { + if len(lit) >= 2 && lit[0] == '"' { + return lit[1 : len(lit)-1] + } + return lit +} + +// foldString reduces a string expression to its value when it is foldable: +// BasicLits, parenthesized forms, `+` of foldable operands, and identifiers +// naming a package-level string const of the same package. +func foldString(expr ast.Expr, consts map[string]string) (string, bool) { + switch e := expr.(type) { + case *ast.BasicLit: + if e.Kind == token.STRING { + return unquote(e.Value), true + } + case *ast.ParenExpr: + return foldString(e.X, consts) + case *ast.BinaryExpr: + if e.Op == token.ADD { + left, lok := foldString(e.X, consts) + right, rok := foldString(e.Y, consts) + if lok && rok { + return left + right, true + } + } + case *ast.Ident: + if v, ok := consts[e.Name]; ok { + return v, true + } + } + return "", false +} + +// derivedNames extracts every qualified name a string binds under one of the +// extension packages: full `Pkg::Name` chains, a string equal to a package +// name (a `package` binding), and `Pkg::` prefix operands of a concatenation +// (a composed prefix, whose composed names cannot be checked one by one). +func derivedNames(value string, packages map[string]bool, names, prefixes map[string]bool) { + for _, m := range qnameRe.FindAllString(value, -1) { + pkg, _, _ := strings.Cut(m, "::") + if packages[pkg] { + names[m] = true + } + } + if packages[value] { + names[value] = true + } + for pkg := range packages { + if strings.HasSuffix(value, pkg+"::") { + prefixes[pkg] = true + } + } +} + +var qnameRe = regexp.MustCompile(`\b([A-Za-z_][A-Za-z0-9_]*)(::[A-Za-z_][A-Za-z0-9_]*)+`) + +// TestEngineContractCoversEveryBoundName derives, from every non-test Go file +// under internal/, cmd/ and tools/, the extension-library qualified names the +// code binds by string, and requires each to be a manifest entry; a manifest +// entry no code derives is stale unless its package composes names under a +// `Pkg::` prefix, which the derivation cannot enumerate. +func TestEngineContractCoversEveryBoundName(t *testing.T) { + packageList := engineContractPkgs(t) + packages := map[string]bool{} + for _, p := range packageList { + packages[p] = true + } + manifest, err := enginecontract.Load(libs.DefaultSource()) + if err != nil { + t.Fatalf("%v", err) + } + entries := manifest.ByName() + + // Parse every non-test .go file under internal/, cmd/ and tools/, + // collecting package-level string consts before folding. + roots := []string{"../../internal", "../../cmd", "../../tools"} + files := map[string][]*ast.File{} + fset := token.NewFileSet() + for _, root := range roots { + err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + if d.Name() == "testdata" { + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") { + return nil + } + file, err := parser.ParseFile(fset, p, nil, 0) + if err != nil { + return err + } + files[filepath.Dir(p)] = append(files[filepath.Dir(p)], file) + return nil + }) + if err != nil { + t.Fatalf("walking %s: %v", root, err) + } + } + constsByFile := stringConsts(files) + + names := map[string]bool{} + prefixes := map[string]bool{} + for _, fsetFiles := range files { + for _, file := range fsetFiles { + consts := constsByFile[file] + ast.Inspect(file, func(n ast.Node) bool { + lit, ok := n.(*ast.BasicLit) + if ok && lit.Kind == token.STRING { + derivedNames(unquote(lit.Value), packages, names, prefixes) + return true + } + bin, ok := n.(*ast.BinaryExpr) + if ok && bin.Op == token.ADD { + if v, folded := foldString(bin, consts); folded { + derivedNames(v, packages, names, prefixes) + } + // Non-foldable concatenations still contribute their + // foldable operands as prefixes and strings. + for _, side := range []ast.Expr{bin.X, bin.Y} { + if v, folded := foldString(side, consts); folded { + derivedNames(v, packages, names, prefixes) + } + } + } + ident, ok := n.(*ast.Ident) + if ok { + if v, c := consts[ident.Name]; c { + derivedNames(v, packages, names, prefixes) + } + } + return true + }) + } + } + + update := "update engine-contract.json upstream in Open-MBEE/OpenSysML-Extensions-Library, bump scripts/extension-libraries-pin.sh, and run scripts/sync-extension-libraries.sh" + + sortedNames := make([]string, 0, len(names)) + for n := range names { + sortedNames = append(sortedNames, n) + } + sort.Strings(sortedNames) + for _, n := range sortedNames { + if _, ok := entries[n]; !ok { + t.Errorf("OpenSysML binds %s but engine-contract.json does not list it; %s", n, update) + } + } + prefixList := make([]string, 0, len(prefixes)) + for p := range prefixes { + prefixList = append(prefixList, p) + } + sort.Strings(prefixList) + t.Logf("packages composing names under a Pkg:: prefix: %v", prefixList) + for _, e := range manifest.Entries { + if names[e.Name] { + continue + } + pkg, _, _ := strings.Cut(e.Name, "::") + if prefixes[pkg] { + continue + } + t.Errorf("engine-contract.json lists %s but no code derives it; stale entries come out upstream; %s", e.Name, update) + } +} diff --git a/tests/testutil/enginecontract/enginecontract.go b/tests/testutil/enginecontract/enginecontract.go new file mode 100644 index 0000000000..d93acb3193 --- /dev/null +++ b/tests/testutil/enginecontract/enginecontract.go @@ -0,0 +1,114 @@ +// Package enginecontract reads the vendored engine-contract.json: the +// qualified names in the extension libraries that the engine binds to. +// Tests in internal/ and tests/hygiene share it so the manifest is parsed the +// same way everywhere it gates. +package enginecontract + +import ( + "encoding/json" + "fmt" + "path" + "sort" + "strings" +) + +// Dir is the vendored directory inside the bundled library that the upstream +// files map to. +const Dir = "OpenSysML Libraries" + +// File is the manifest inside Dir. +const File = Dir + "/engine-contract.json" + +// Kinds are the manifest's entry kinds. +const ( + KindPackage = "package" + KindFunction = "function" + KindMetadata = "metadata" + KindDocument = "document" + KindElement = "element" +) + +// Source reads the bundled library the way libs.Source does. +type Source interface { + List() []string + Read(name string) ([]byte, error) +} + +// Entry is one manifest record. +type Entry struct { + Name string `json:"name"` + Kind string `json:"kind"` + Parameters []string `json:"parameters"` + Attributes []string `json:"attributes"` +} + +// Manifest is the parsed engine-contract.json. +type Manifest struct { + Contract int `json:"contract"` + Entries []Entry `json:"entries"` +} + +// ByName indexes the manifest's entries by their qualified name. +func (m Manifest) ByName() map[string]Entry { + out := make(map[string]Entry, len(m.Entries)) + for _, e := range m.Entries { + out[e.Name] = e + } + return out +} + +// Load reads and validates the manifest from src. +func Load(src Source) (Manifest, error) { + var m Manifest + data, err := src.Read(File) + if err != nil { + return m, fmt.Errorf("reading %s: %w", File, err) + } + if err := json.Unmarshal(data, &m); err != nil { + return m, fmt.Errorf("parsing %s: %w", File, err) + } + names := make([]string, 0, len(m.Entries)) + for _, e := range m.Entries { + names = append(names, e.Name) + switch e.Kind { + case KindFunction: + if e.Parameters == nil { + return m, fmt.Errorf("%s: function entries require parameters", e.Name) + } + case KindMetadata: + if e.Attributes == nil { + return m, fmt.Errorf("%s: metadata entries require attributes", e.Name) + } + case KindPackage, KindDocument, KindElement: + default: + return m, fmt.Errorf("%s: unknown kind %q", e.Name, e.Kind) + } + } + for i, name := range names { + if i > 0 && name <= names[i-1] { + return m, fmt.Errorf("%s: entry %q is out of order or duplicated", File, name) + } + } + return m, nil +} + +// Packages lists the extension package names the vendored directory carries: +// the stems of its .sysml and .kerml files. +func Packages(src Source) ([]string, error) { + var out []string + for _, name := range src.List() { + dir, base := path.Split(name) + if dir != Dir+"/" { + continue + } + ext := path.Ext(base) + if ext == ".sysml" || ext == ".kerml" { + out = append(out, strings.TrimSuffix(base, ext)) + } + } + sort.Strings(out) + if len(out) == 0 { + return nil, fmt.Errorf("%s holds no .sysml or .kerml file", Dir) + } + return out, nil +}