diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8a5993f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,113 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + validate: + name: Validate the extension libraries + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch the pull request base + if: github.event_name == 'pull_request' + run: git fetch origin "${{ github.base_ref }}" + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Cache the pilot validator + uses: actions/cache@v4 + with: + path: build/pilot-validator + key: pilot-validator-${{ hashFiles('scripts/pilot-pin.sh', 'scripts/download-pilot-validator.sh', 'scripts/download-pilot-sysml-validator.sh', 'scripts/verify-pilot-pin.sh') }} + restore-keys: pilot-validator- + + - name: Verify the pilot pin + run: ./scripts/verify-pilot-pin.sh + + - name: Validate the libraries + run: ./scripts/validate-libraries.sh + + - name: Check the README library table + run: ./scripts/check-readme-table.sh + + - name: Check the engine contract + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + ./scripts/check-engine-contract.sh --base "origin/${{ github.base_ref }}" + else + ./scripts/check-engine-contract.sh + fi + + - name: Build the project archive + run: ./scripts/build-kpar.py --version "0.0.0-dev.${{ github.run_number }}+${GITHUB_SHA::7}" --output opensysml-extension-libraries-dev.kpar + + - name: Upload the project archive + uses: actions/upload-artifact@v4 + with: + name: opensysml-extension-libraries-dev + path: opensysml-extension-libraries-dev.kpar + + opensysml-mirror: + name: OpenSysML against these libraries (advisory) + runs-on: ubuntu-latest + continue-on-error: true + timeout-minutes: 90 + steps: + - name: Check out this repository + uses: actions/checkout@v4 + with: + path: extensions + + - name: Check out OpenSysML + uses: actions/checkout@v4 + with: + repository: Open-MBEE/OpenSysML + ref: develop + path: opensysml + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: opensysml/go.mod + + - name: Install z3 + run: | + sudo apt-get update + sudo apt-get install -y z3 + + - 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: Run OpenSysML against these libraries + env: + EXTENSIONS_DIR: ${{ github.workspace }}/extensions + run: | + cd "$GITHUB_WORKSPACE/opensysml" + if [ ! -f scripts/sync-extension-libraries.sh ]; then + echo "OpenSysML does not yet vendor from this repository (no sync-extension-libraries.sh); skipping." + exit 0 + fi + scripts/sync-extension-libraries.sh --source "$EXTENSIONS_DIR" + make stdlib-snapshot-check || make stdlib-snapshot + go test ./internal/workspace/libs/... ./internal/exec/... ./internal/ir/... ./internal/doc/... ./internal/semantic/... ./internal/check/... ./internal/translate/... ./tests/hygiene/... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..676f862 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,46 @@ +name: Release + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +permissions: + contents: write + +jobs: + release: + name: Build and attach the project archive + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Verify the pilot pin + run: ./scripts/verify-pilot-pin.sh + + - name: Validate the libraries + run: ./scripts/validate-libraries.sh + + - name: Build the project archive + run: | + version="${GITHUB_REF_NAME#v}" + ./scripts/build-kpar.py --version "$version" --output "opensysml-extension-libraries-${version}.kpar" + + - name: Create the release + env: + GH_TOKEN: ${{ github.token }} + run: | + version="${GITHUB_REF_NAME#v}" + kpar="opensysml-extension-libraries-${version}.kpar" + gh release create "$GITHUB_REF_NAME" "$kpar" \ + --verify-tag \ + --title "OpenSysML Extension Libraries $GITHUB_REF_NAME" \ + --notes "KerML 10.3 project archive of the extension libraries at $GITHUB_REF_NAME, validated against the pinned OMG SysML v2 Pilot Implementation." diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4252196 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +*.kpar diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a79260f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing + +## Proposing a library + +Open an issue or pull request stating the library's purpose, its intended +consumers, and its portability (any SysML v2 tool, or an executing engine). + +## Naming + +- UpperCamelCase; the package name equals the file stem. +- One top-level `library package` per file. +- Never reuse an OMG standard-library package name. +- `.kerml` only for KerML-only content; everything else is `.sysml`. + +## Documentation + +- A top-level `doc` that says NON-NORMATIVE and what the library is for. +- Every public definition documented. + +## Validation + +- `./scripts/validate-libraries.sh` must pass on the pinned pilot with 0 + errors and 0 warnings. +- `validation/allowed-warnings.txt` only shrinks: never add an entry. +- Never write `standard library package` — that keyword is reserved for the + OMG standard library and the pilot validator warns on it. +- A README table row is required; CI checks the table against `libraries/`. +- State the semver impact of your change in the pull request (see Versioning + in the README). + +## The engine contract + +`engine-contract.json` records the qualified names OpenSysML binds to. +Changing or removing a manifest entry is a breaking change: it needs +`contract` bumped, a coordinated Open-MBEE/OpenSysML pull request that +re-pins its vendored copy, and a MAJOR release. New engine bindings are added +to the manifest by the OpenSysML-side pull request that introduces them — +upstream there first. + +## Review + +At least one maintainer approval. Changes OpenSysML's runtime depends on need +a matching OpenSysML pull request that re-pins its vendored copy; edits to +that vendored copy are not accepted in OpenSysML — they come here. + +## Bumping the pilot pin + +Edit `scripts/pilot-pin.sh` (`PILOT_TAG` and `PILOT_COMMIT` together) and +re-run `./scripts/verify-pilot-pin.sh` and `./scripts/validate-libraries.sh`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..128bdf8 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# OpenSysML Extension Libraries + +A community repository of **non-normative** SysML v2 / KerML extension +libraries. They are maintained here and consumed by +[OpenSysML](https://github.com/Open-MBEE/OpenSysML) and any other SysML v2 +tool. They are **not** part of the OMG standard library; some of them are +proposed for standardization (DiagramLayout, IdentityMetadata and +MigrationMetadata say so in their doc text). + +A model that uses one of these libraries is valid SysML v2 that depends on a +non-normative OpenSysML library; other tools resolve it when given the +`libraries/` folder (OpenSysML bundles the same files as its +`OpenSysML Libraries` folder). + +## The libraries + +| Library | File | Description | Portability | +|---|---|---|---| +| AnalysisRecords | AnalysisRecords.sysml | Metadata a run, sweep or sample records itself with: when it ran, the tool and command, verdicts and evaluations. | Executing engine | +| DiagramLayout | DiagramLayout.sysml | Diagram geometry: element placement, edge waypoints and the drawing extent. | Any SysML v2 tool | +| DocumentQueries | DocumentQueries.sysml | Calc definitions (OwnedElements, Descendants, Ancestors, …) that document views evaluate to select elements. | Executing engine | +| IdentityMetadata | IdentityMetadata.sysml | Binds notation to repository element identity. | Any SysML v2 tool | +| MOSA | MOSA.sysml | Modular Open Systems Approach (10 U.S.C. § 4401) vocabulary. | Any SysML v2 tool | +| MigrationMetadata | MigrationMetadata.sysml | What a migration from another modelling language had to make up because the source left it unstated. | Any SysML v2 tool | +| OOSEM | OOSEM.sysml | Object-Oriented Systems Engineering Method artefacts, viewpoints and product views. | Any SysML v2 tool | +| OpenSysMLMathFunctions | OpenSysMLMathFunctions.kerml | exp, ln, log, atan2, ceiling and the Integer quotient the Kernel Function Library omits. | Executing engine | +| RandomFunctions | RandomFunctions.kerml | Seeded random draws: uniform, uniformInteger, triangular, normal. | Executing engine | +| Simulation | Simulation.sysml | How a behavior is meant to be run: run count, draw resolution, clock, Monte Carlo. | Executing engine | +| StateMachines | StateMachines.sysml | Pseudostates (choice, junction, shallow/deep history) the grammar has no production for. | Executing engine | +| StateSpaceIntegration | StateSpaceIntegration.sysml | Fixed-step (Euler/RK4) running of StateSpaceRepresentation dynamics. | Executing engine | +| Stochastic | Stochastic.sysml | Probability metadata for branches; a tool unaware of it runs the branches as an unweighted choice. | Executing engine | +| SysMLValidation | SysMLValidation.sysml | KerML/SysML abstract-syntax validation constraints over the reflective metamodel. | Executing engine | + +Portability "Executing engine" means an executing engine gives the library its +behavior; the libraries still parse and resolve in any tool — only their +behavior needs an engine. + +## Layout + +`libraries/` is flat: one `library package` per file, and the file stem equals +the package name. SysML resolves libraries by package name, not path, so any +tool can load the directory as-is. The folder these files used to live in, +`OpenSysML Libraries/`, was OpenSysML's library-tier folder; the history here +was imported from Open-MBEE/OpenSysML, and old `#N` references in commit +messages point at that repository. + +## Using the libraries + +Every tool also needs the OMG standard library (`sysml.library`). + +- **OMG pilot batch validator** — + `validate-sysml-batch --library libraries ` + (`validate-sysml --library DIR [--root DIR] FILE|DIR...`). The validator in + this repository (`./scripts/download-pilot-sysml-validator.sh`) builds the + same program. +- **Pilot Eclipse** — import this repository into the workspace as a project + alongside `sysml.library`, so the editor indexes it the same way. +- **SysON** — import the `.sysml` files into the project, or into a library + project it references if your SysON version supports that; see SysON's + documentation. SysON does not execute behavior, so the portable libraries + are the ones that matter there. +- **sysml-toolkit** — + `sysmlv2 check --lib libraries/*.sysml libraries/*.kerml model.sysml`, + or pass the release `.kpar` (the CLI expands `.kpar` inputs). +- **OpenSysML** — bundled: it vendors a pinned copy at + `internal/workspace/libs/stdlib/OpenSysML Libraries/`, pinned by + `scripts/extension-libraries-pin.sh` and refreshed by + `scripts/sync-extension-libraries.sh`. Edits land here first. + +## Versioning + +Releases are semver tags `vX.Y.Z` on `main`: + +- **MAJOR** — removing or renaming a library or a public element, or changing + its meaning. +- **MINOR** — adding a library or element. +- **PATCH** — documentation and doc-comment changes and fixes that keep names + and meaning. + +Each release attaches a `.kpar` project archive. Consumers pin a tag or +commit. + +## Engine contract + +`engine-contract.json` lists the qualified names in `libraries/` that +OpenSysML's engine binds to — the functions its runtime registry implements, +the metadata and features it reads, the DocumentQueries elements its document +and query plans compile, and the names the SysML v1 migrator writes into user +models. OpenSysML binds by qualified name, so those names are an API: removing, +renaming or changing an entry is a breaking change and needs `contract` +bumped, a coordinated Open-MBEE/OpenSysML pull request that re-pins its +vendored copy, and a MAJOR release. Additions are MINOR. + +`./scripts/check-engine-contract.sh` validates the manifest and resolves every +entry against `libraries/` on the pinned pilot; in pull requests it also diffs +the manifest against the base and fails on unannounced breaking changes. + +## Validation + +`./scripts/validate-libraries.sh` runs the pinned OMG pilot batch validator +(`scripts/pilot-pin.sh`) over `libraries/` and requires zero errors and zero +warnings. Four warnings are temporarily allow-listed in +`validation/allowed-warnings.txt` — the four files that still declare +`standard library package`; an upstream change to `library package` is being +ported and those entries will be deleted. The allow-list only shrinks. + +## License + +Apache-2.0. Every library was authored in OpenSysML; no OMG material is +redistributed here. diff --git a/engine-contract.json b/engine-contract.json new file mode 100644 index 0000000..372004f --- /dev/null +++ b/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/libraries/AnalysisRecords.sysml b/libraries/AnalysisRecords.sysml new file mode 100644 index 0000000..208a044 --- /dev/null +++ b/libraries/AnalysisRecords.sysml @@ -0,0 +1,86 @@ +library package AnalysisRecords { + /* NON-NORMATIVE OpenSysML run-record vocabulary; + * not part of the OMG SysML or KerML standard library. */ + + private import ScalarValues::*; + + /* RecordedRun is the metadata an analysis run, sweep or sample records + * itself with: when it ran, the tool and command that ran it, and the + * shape of the run it records. */ + metadata def RecordedRun { + /* runAt is the timestamp of the run, written in UTC. */ + attribute runAt : String; + /* tool names the program that made the run. */ + attribute tool : String; + /* command is the invocation text that made the run. */ + attribute command : String; + /* kind is the run shape recorded: "run", "trade", "sweep", "runs" or + * "sample" — the conclusion a Monte-Carlo sample made. */ + attribute kind : String; + /* tools names each external tool a run reached, one element per call in call + * order: its name and version, the manifest file, and the command composed. */ + attribute tools : String[0..*] ordered nonunique; + } + + /* VerdictRecord is what one check of a recorded run decided: an + * objective's required conditions, an assertion in the case's body, + * or a verification case's own verdict and its subcases'. */ + part def VerdictRecord { + /* kind is "objective", "assertion", "verification" or "subcase". */ + attribute kind : String; + /* name names the objective or asserted constraint, the verification + * case, or spells an anonymous assertion's condition. */ + attribute name : String; + /* status is what the check decided. */ + attribute status : String; + /* detail is the violated condition of a failed check, or why an + * undecided one could not be evaluated. */ + attribute detail : String; + } + + /* EvaluationRecord is one application of a case's calc to an + * alternative, as a trade study's evaluation records it. */ + part def EvaluationRecord { + /* function is the qualified name of the calc applied. */ + attribute function : String; + /* alternative is what the calc was applied to. */ + attribute alternative : String; + /* score is the value the evaluation computed. */ + attribute score : Real; + /* result is the computed value's text. */ + attribute result : String; + /* selected marks the evaluation the case picked. */ + attribute selected : Boolean; + /* tied marks an evaluation computing what the selected did. */ + attribute tied : Boolean; + /* error is why an evaluation computed nothing. */ + attribute error : String; + } + + /* AnalysisRun is the record one run of an analysis case makes: the + * values its inputs bound and its outputs took, the object it ran on, + * and what each of its checks decided and alternatives evaluated. */ + part def AnalysisRun { + /* caseName is the qualified name of the case that ran. */ + attribute caseName : String; + /* kind is the run shape this record makes. */ + attribute kind : String; + /* 'objective' is the status of the run's objective verdict, + * "undecided" when the case declares none. */ + attribute 'objective' : String; + /* iteration is the run's position in a sweep or sample. */ + attribute iteration : Integer; + /* 'subject' is the object the run was made on. */ + ref part 'subject'; + /* subjectName is the subject object's text. */ + attribute subjectName : String; + /* verdict is what the verification body's verdict value decided — + * "pass", "fail", "inconclusive" or "error" — unset for a case + * that is not a verification. */ + attribute verdict : String; + /* verdicts are what the run's checks decided, in order. */ + part verdicts : VerdictRecord[0..*]; + /* evaluations are the calc applications the run made, in order. */ + part evaluations : EvaluationRecord[0..*]; + } +} diff --git a/libraries/DiagramLayout.sysml b/libraries/DiagramLayout.sysml new file mode 100644 index 0000000..3fc5f80 --- /dev/null +++ b/libraries/DiagramLayout.sysml @@ -0,0 +1,72 @@ +standard library package DiagramLayout { + doc /* Diagram geometry in notation: where a view draws an element, the + * waypoints an edge is drawn through and the extent of the drawing + * surface. Units are pixels, y down, origin at the top left. + * Non-normative OpenSysML extension, proposed for standardization; + * see the design record. */ + + metadata def Layout { + doc /* Where the annotated element is drawn: the top-left corner of its + * box, an optional fixed size, and whether its body is folded. */ + attribute x : ScalarValues::Real; + attribute y : ScalarValues::Real; + attribute width : ScalarValues::Real[0..1]; + attribute height : ScalarValues::Real[0..1]; + attribute collapsed : ScalarValues::Boolean[0..1]; + } + + metadata def Route { + doc /* The waypoints a connection, transition, succession or flow is + * drawn through, in order, flattened as x0, y0, x1, y1, ... */ + attribute points : ScalarValues::Real[0..*] ordered nonunique; + } + + metadata def Canvas { + doc /* The unit and extent of a view's drawing surface. */ + attribute unit : ScalarValues::String[0..1]; + attribute width : ScalarValues::Real[0..1]; + attribute height : ScalarValues::Real[0..1]; + } + + metadata def Style { + doc /* How the annotated element is drawn: the colours of its fill, + * outline and text as "#RRGGBB", and the face, size in points and + * weight of its text. Each is optional; the drawing style supplies + * what is left unstated. */ + attribute fill : ScalarValues::String[0..1]; + attribute line : ScalarValues::String[0..1]; + attribute text : ScalarValues::String[0..1]; + attribute font : ScalarValues::String[0..1]; + attribute fontSize : ScalarValues::Real[0..1]; + attribute bold : ScalarValues::Boolean[0..1]; + attribute italic : ScalarValues::Boolean[0..1]; + } + + metadata def Note { + doc /* A note drawn beside the annotated element and anchored to it, + * or free on the drawing surface when it annotates the view: its + * text, the top-left corner of its box and an optional fixed + * size, in the units of Layout. */ + attribute text : ScalarValues::String; + attribute x : ScalarValues::Real; + attribute y : ScalarValues::Real; + attribute width : ScalarValues::Real[0..1]; + attribute height : ScalarValues::Real[0..1]; + } + + metadata def Picture { + doc /* A picture drawn on the drawing surface of the view it annotates: + * the file it is read from, relative to the file the view is + * written in, the top-left corner and size of the box it fills, + * in the units of Layout, an optional alternative text, and + * whether it is drawn over the elements it overlaps rather than + * under them. */ + attribute location : ScalarValues::String; + attribute x : ScalarValues::Real; + attribute y : ScalarValues::Real; + attribute width : ScalarValues::Real; + attribute height : ScalarValues::Real; + attribute alt : ScalarValues::String[0..1]; + attribute above : ScalarValues::Boolean[0..1]; + } +} diff --git a/libraries/DocumentQueries.sysml b/libraries/DocumentQueries.sysml new file mode 100644 index 0000000..12be0f2 --- /dev/null +++ b/libraries/DocumentQueries.sysml @@ -0,0 +1,416 @@ +library package DocumentQueries { + /* NON-NORMATIVE OpenSysML document-query vocabulary; + * not part of the OMG SysML or KerML standard library. */ + + private import KerML::Root::Element; + private import ScalarValues::*; + + abstract calc def Query { + return result : Element[0..*] ordered; + } + + calc def OwnedElements { + in source : Element[0..*] ordered; + return result : Element[0..*] ordered; + } + + /* Descendants and Ancestors walk ownership from source to maxDepth levels; + * maxDepth = null (the default) walks the whole tree. */ + calc def Descendants { + in source : Element[0..*] ordered; + in maxDepth : Integer[0..1] default null; + return result : Element[0..*] ordered; + } + + calc def Ancestors { + in source : Element[0..*] ordered; + in maxDepth : Integer[0..1] default null; + return result : Element[0..*] ordered; + } + + /* Named: the model elements the qualified names denote, in the order given; + * a name that denotes nothing is an error. */ + calc def Named { + in qualifiedName : String[1..*] ordered; + return result : Element[0..*] ordered; + } + + /* Objects: the objects the session holds that are of the named type, + * each nested object under its path (`car.wheels[2]`). Requires a session. */ + calc def Objects { + in type : String[1]; + return result : Element[0..*] ordered; + } + + /* Verdicts: one row per assertion checked on each source row's object and the + * objects it holds (a session object as it stands, an element as declared); + * kind selects all, constraint, requirement, satisfaction or verification. */ + calc def Verdicts { + in source : Element[0..*] ordered; + in kind : String[1] default "all"; + return result : Element[0..*] ordered; + } + + /* The properties of a Verdicts row, for Project and column expressions + * (`Verdict::verdict`); a row also answers its assertion's own properties by + * name. */ + attribute def Verdict { + ref assertion : Element[1]; + attribute kind : String[1]; + ref carrier : Element[0..1]; + attribute path : String[1]; + attribute verdict : String[1]; + attribute condition : String[0..1]; + attribute reason : String[0..1]; + attribute 'verification' : String[0..*] ordered; + } + + /* States: one row per active leaf state of each source row's object (a + * session object, or an element whose objects the session holds), machines + * in the object's order and leaves in region order; an object with no state + * machine is refused. Requires a session. */ + calc def States { + in source : Element[0..*] ordered; + return result : Element[0..*] ordered; + } + + /* InState: the objects the session holds whose state machine is in the + * named state, a leaf or a state enclosing one, by name or dotted path + * (`on.run`); a name no machine declares is refused. Requires a session. */ + calc def InState { + in name : String[1]; + return result : Element[0..*] ordered; + } + + /* The properties of a States row, for Project and column expressions + * (`State::name`); a row also answers its state declaration's own + * properties. object and 'state' are elements: project them by name. */ + attribute def State { + ref object : Element[1]; + attribute path : String[1]; + attribute machine : String[1]; + attribute name : String[1]; + attribute statePath : String[1]; + ref 'state' : Element[0..1]; + attribute region : String[1]; + attribute enclosing : String[0..*] ordered; + } + + /* Events: the session's trace as a relation in the order recorded, one row + * per accept, send, transition fired, state entry, exit or do step, choice + * point drawn or guard left unevaluated; source keeps the rows of the given + * objects (every object when null), kind the kinds named (`all`, or kinds + * separated by `,`), since and before the instants in [since, before) as + * durations or bare numbers of the clock's seconds. Requires a session + * recording a trace. */ + calc def Events { + in source : Element[0..*] ordered default null; + in kind : String[1] default "all"; + in since : ScalarValue[0..1] default null; + in before : ScalarValue[0..1] default null; + return result : Element[0..*] ordered; + } + + /* The properties of an Events row, for Project and column expressions + * (`Event::time`, `Event::'event'`); a row also answers the properties of + * the behavior that made it. object and target are elements: project them + * by name. */ + attribute def Event { + attribute kind : String[1]; + attribute time : ScalarValue[1]; + ref object : Element[0..1]; + attribute path : String[0..1]; + attribute machine : String[0..1]; + attribute 'state' : String[0..1]; + attribute 'from' : String[0..1]; + attribute 'to' : String[0..1]; + ref target : Element[0..1]; + attribute 'event' : String[0..1]; + attribute payload : String[0..*] ordered; + attribute alternatives : String[0..*] ordered; + attribute taken : String[0..1]; + attribute text : String[1]; + } + + calc def RelatedElements { + in source : Element[0..*] ordered; + /* relationshipKind: "specialization", "subsetting", "redefinition", + * "typing", "connection", "allocation", "satisfaction", "verification", + * "derivation" or "refinement"; direction: "outgoing" or "incoming". */ + in relationshipKind : String[1]; + in direction : String[1]; + /* maxDepth: how many relationships to follow; null (the default) is unbounded. */ + in maxDepth : Integer[0..1] default null; + return result : Element[0..*] ordered; + } + + /* WhereType keeps the elements that are of any of the named types. */ + calc def WhereType { + in source : Element[0..*] ordered; + in type : String[1..*] ordered; + return result : Element[0..*] ordered; + } + + /* WhereMetadata keeps the elements annotated with any of the named metadata definitions. */ + calc def WhereMetadata { + in source : Element[0..*] ordered; + in 'metadata' : String[1..*] ordered; + return result : Element[0..*] ordered; + } + + calc def WhereName { + in source : Element[0..*] ordered; + in operator : String[1]; + in value : String[1]; + return result : Element[0..*] ordered; + } + + /* WhereText keeps the projected rows whose text in any of the named columns — every column when none is named — satisfies the comparison. */ + calc def WhereText { + in source : Element[0..*] ordered; + in columns : String[0..*] ordered = null; + in operator : String[1]; + in value : String[1]; + return result : Element[0..*] ordered; + } + + calc def WhereFeature { + in source : Element[0..*] ordered; + in 'feature' : String[1]; + in operator : String[1]; + in value : String[1]; + return result : Element[0..*] ordered; + } + + calc def OrderBy { + in source : Element[0..*] ordered; + in property : String[1]; + in direction : String[1]; + in missing : String[1]; + in multiple : String[1]; + return result : Element[0..*] ordered; + } + + /* Distinct: seq without its repeated values, the first of each kept in the + * order met; a cell applies it to a sequence as `->DocumentQueries::Distinct()` + * beside the KerML sequence functions (`->SequenceFunctions::size()`, + * `->ControlFunctions::select {in e; …}`). */ + calc def Distinct { + in seq : Base::Anything[0..*] ordered nonunique; + return result : Base::Anything[0..*] ordered; + } + + /* One computed table column: exactly one of expression, cell, or path. + * expression is evaluated in query scope; cell receives each row as its + * parameter; path reads a dotted member path from the row. */ + attribute def ColumnSpec; + + /* A cell reads row properties through its typed row parameter + * (e.g. row.mass); an expression uses query-scope features such as + * Subsystem::mass. Both support +, -, *, / arithmetic, + * string concatenation with +, and ?? for absent-value defaults. + * A feature chain (e.g. stat.runs or 'Monte Carlo'.runs) reads a + * feature of a member nested in the row element, each segment + * resolved against the members of the element reached so far — + * own members before inherited ones; a row lacking a segment is + * absent on that row, a member declaring no value is an empty + * cell, and a member holding more values than its multiplicity + * admits fails the column as a direct feature column does. A cell + * over a metadata row reads its direct features only. A property + * string of Project's properties or OrderBy's property may hold + * the same path ("stat.runs"). + * A row not conforming to the feature's declaring type reads it as + * absent; a failing expression, an absent result not defaulted + * with ??, or a multi-valued result fails the query with a typed + * error naming the row and column. */ + calc def Column { + in name : String[1]; + in expression : ScalarValue[0..1]; + in calc cell[0..1] { in row : Base::Anything[1]; return : ScalarValue[0..1]; } + in path : String[0..1]; + return result : ColumnSpec[1]; + } + + calc def Project { + in source : Element[0..*] ordered; + in properties : String[0..*] ordered default null; + in columns : ColumnSpec[0..*] ordered default null; + return result : Element[0..*] ordered; + } + + abstract part def Document { + attribute title : String[1]; + } + + abstract part def Section { + attribute title : String[1]; + } + + abstract part def ContentBlock; + + part def Paragraph :> ContentBlock { + attribute text : String[0..1]; + } + + /* Inline runs compose a paragraph in declaration order, joined by + * single spaces. */ + abstract part def Run; + + part def Span :> Run { + attribute text : String[1]; + /* style: "plain", "emphasis", "strong", "code" or "math"; a math + * span's text is LaTeX, typeset inline. */ + attribute style : String[0..1]; + } + + part def Link :> Run { + attribute text : String[1]; + attribute target : String[1]; + } + + /* Ref links to a named content block of this or another document, to + * another document itself, or to a model element outside every document + * (a usage by name, any element as `E.metadata`); text defaults to the + * target's title, caption or name. */ + part def Ref :> Run { + attribute text : String[0..1]; + ref target : Element[1]; + } + + /* Column runs style a query-backed paragraph's or list's text: each + * result row renders one run per column run, in declaration order. */ + abstract part def ColumnRun; + + part def SpanColumn :> ColumnRun { + /* column: the projected column whose values become the run text. */ + attribute column : String[1]; + /* style: "plain", "emphasis", "strong", "code" or "math". */ + attribute style : String[0..1]; + /* styleColumn: a projected column supplying each row's style. */ + attribute styleColumn : String[0..1]; + } + + part def LinkColumn :> ColumnRun { + /* column: the projected column whose values become the link text. */ + attribute column : String[1]; + /* targetColumn: a projected column supplying each row's one link + * destination. */ + attribute targetColumn : String[1]; + } + + part def Table :> ContentBlock { + attribute caption : String[0..1]; + /* groupBy: a projected column name; rows are grouped by its value + * into subtables in order of first appearance. */ + attribute groupBy : String[0..1]; + /* columnWidths: the relative widths of the projected columns in order, + * in the source's units; 0 sizes a column automatically. Renderers + * honour them proportionally. */ + attribute columnWidths : Integer[0..*] ordered nonunique; + /* columnLabels: the headings of the projected columns in order, where + * they differ from the column names; "" heads a column by its name. */ + attribute columnLabels : String[0..*] ordered nonunique; + } + + part def List :> ContentBlock { + attribute style : String[0..1]; + } + + /* Definitions renders each row of its query as one term with its + * description(s): prose from the model rather than a table of it. */ + part def Definitions :> ContentBlock { + /* term: the projected column whose value names each entry. */ + attribute term : String[1]; + /* description: the projected column whose values describe it. */ + attribute description : String[1]; + } + + /* Formula typesets one LaTeX expression as display mathematics on a line + * of its own; a named formula is a Ref target. */ + part def Formula :> ContentBlock { + attribute source : String[1]; + attribute caption : String[0..1]; + } + + /* Image shows an image file: location is a path relative to the document's + * source file, or an http(s)/file URL; alt is its text alternative. */ + part def Image :> ContentBlock { + attribute location : String[1]; + attribute caption : String[0..1]; + attribute alt : String[0..1]; + } + + part def Diagram :> ContentBlock { + attribute caption : String[0..1]; + /* kind: a supported rendering kind ("tree", "interconnection", + * "state", "action", "table" or "sequence"); required when source + * is a plain element, stated by the view when source is a view. */ + attribute kind : String[0..1]; + /* direction: "TB", "LR", "RL" or "BT", for kinds drawn as graphs. */ + attribute direction : String[0..1]; + /* palette: "okabe-ito", "tol-bright", "tol-muted", "tol-light", + * "brewer-set2", "brewer-dark2", "viridis" or "cividis"; fills the + * nodes of a DOT or PlantUML diagram by keyword family, black and white + * when absent. */ + attribute palette : String[0..1]; + /* ports: "minimal" or "full"; how much of a part's ports an + * interconnection diagram draws — those its connectors end at, each a + * small square on the part's border named beside it, or every port, + * labelled name : Type. Minimal when absent. */ + attribute ports : String[0..1]; + ref source : Element[0..1]; + } + + /* A relationship-derived Project column: the elements RelatedElements + * reaches from each row, as a "list" (default), a "count" or "any"; + * maxDepth = null is unbounded, and targets, when given, keeps only the + * reached elements among them. */ + calc def RelatedColumn { + in name : String[1]; + in relationshipKind : String[1]; + in direction : String[1]; + in maxDepth : Integer[0..1] default null; + in aggregate : String[1] default "list"; + in targets : Element[0..*] ordered default null; + return result : ColumnSpec[1]; + } + + /* WhereRelated: the source rows with at least one element reachable by the + * relationship kind and direction of RelatedElements within maxDepth + * (exists = true), or with none (exists = false); maxDepth = null is unbounded. */ + calc def WhereRelated { + in source : Element[0..*] ordered; + in relationshipKind : String[1]; + in direction : String[1]; + in maxDepth : Integer[0..1] default null; + in exists : Boolean[1] default true; + return result : Element[0..*] ordered; + } + + /* Except: the source rows whose element, object, verdict, state or event + * is not among exclude, once each in source order. */ + calc def Except { + in source : Element[0..*] ordered; + in exclude : Element[0..*] ordered; + return result : Element[0..*] ordered; + } + + /* Union: the source rows, then the rows of other not already present, in + * order with duplicates removed. */ + calc def Union { + in source : Element[0..*] ordered; + in other : Element[0..*] ordered; + return result : Element[0..*] ordered; + } + + /* Tree: the source rows arranged as a containment tree in pre-order, each + * row nested under the nearest row containing it — its nearest owner among + * the rows, or the individual whose part it is — at the depth the table + * indents it by; rows of ancestors join as intermediate levels where a + * source row nests under them. */ + calc def Tree { + in source : Element[0..*] ordered; + in ancestors : Element[0..*] ordered = null; + return result : Element[0..*] ordered; + } +} diff --git a/libraries/IdentityMetadata.sysml b/libraries/IdentityMetadata.sysml new file mode 100644 index 0000000..815716a --- /dev/null +++ b/libraries/IdentityMetadata.sysml @@ -0,0 +1,17 @@ +standard library package IdentityMetadata { + doc /* Binds notation to repository identity. Non-normative OpenSysML + * extension, proposed for standardization; see the design record. */ + + metadata def ElementId { + doc /* The annotated element is the repository element with this id. */ + attribute id : ScalarValues::String; + } + + metadata def ProjectRef { + doc /* Elements below the annotated namespace that carry ElementId + * resolve against this project and branch. */ + attribute projectId : ScalarValues::String; + attribute branch : ScalarValues::String[0..1]; + attribute org : ScalarValues::String[0..1]; + } +} diff --git a/libraries/MOSA.sysml b/libraries/MOSA.sysml new file mode 100644 index 0000000..b05f834 --- /dev/null +++ b/libraries/MOSA.sysml @@ -0,0 +1,523 @@ +library package MOSA { + doc + /* + * NON-NORMATIVE OpenSysML vocabulary for the Modular Open Systems Approach (MOSA), the + * integrated business and technical strategy 10 U.S.C. § 4401 requires of United States + * Department of Defense major defense acquisition programs. Its concepts and their names + * are the statute's: the major system platform, the major system components mounted on + * it, the modular systems that can be separated and recombined, and the modular system + * interfaces between them, each verified against a widely supported, consensus-based + * standard or documented in machine-readable form, with the rights in technical data that + * let a component be competed, upgraded or replaced over the life cycle. + * Each MOSA artefact gets a definition specialising its native SysML type, a base usage, and + * a semantic-metadata keyword applying both at once: `#majorSystemComponent part radar : Radar;`. + * Standards conformance, data rights, interface control and conformance assessment are + * metadata over the standard model. The library's products are view definitions on the + * standard view definitions and document queries for an interface control document. + * Measures, requirement derivation and status/rationale metadata reuse the standard domain + * libraries, re-exported here. + */ + + private import ScalarValues::Integer; + private import ScalarValues::String; + private import Metaobjects::SemanticMetadata; + private import KerML::Root::Element; + private import Parts::Part; + private import Parts::parts; + private import Items::Item; + private import Items::items; + private import Interfaces::Interface; + private import Interfaces::interfaces; + private import Interfaces::BinaryInterface; + private import Interfaces::binaryInterfaces; + private import Requirements::RequirementCheck; + private import Requirements::requirementChecks; + private import StandardViewDefinitions::*; + private import Views::asTreeDiagram; + private import Views::asInterconnectionDiagram; + private import Views::asElementTable; + private import DocumentQueries::*; + + public import ParametersOfInterestMetadata::*; + public import RequirementDerivation::*; + public import ModelingMetadata::*; + + /* Modular design: the statutory structure of a major system */ + + part def MajorSystemPlatform :> Part { + doc /* A MajorSystemPlatform is the highest-level structure of a major weapon system that is not + * itself mounted on a higher-level structure, and on which major system components are + * mounted or installed (10 U.S.C. § 4401(b)). */ + } + + abstract part majorSystemPlatforms : MajorSystemPlatform[0..*] nonunique :> parts; + + part def MajorSystemComponent :> Part { + doc /* A MajorSystemComponent is a high-level subsystem or assembly — hardware, software or an + * integrated assembly of components — that is mounted on or installed in a major system + * platform, or is itself a major system, and that is designed to be severable and replaced + * incrementally over the life cycle. */ + } + + abstract part majorSystemComponents : MajorSystemComponent[0..*] nonunique :> parts; + + part def ModularSystem :> Part { + doc /* A ModularSystem is a system or component that executes without requiring the coincident + * execution of other systems or components, communicates across component boundaries through + * interfaces, and functions as a module that can be separated, recombined and connected with + * other systems or components to achieve various effects, missions or capabilities. + * It is what the MOSA Implementation Guidebook calls a MOSA module. */ + } + + abstract part modularSystems : ModularSystem[0..*] nonunique :> parts; + + /* Modular system interfaces: the shared boundaries a modular design designates */ + + enum def InterfaceCharacteristicKind { + doc /* The physical, logical and functional characteristics the statute names as defining a + * modular system interface. An interface may have several. */ + electrical; + mechanical; + fluidic; + optical; + radioFrequency; + data; + networking; + software; + } + + abstract interface def ModularSystemInterface :> Interface { + doc /* A ModularSystemInterface is a shared boundary between major systems, major system + * components or modular systems, defined by physical, logical and functional characteristics + * such as electrical, mechanical, fluidic, optical, radio-frequency, data, networking or + * software elements. MOSA verifies each against a consensus-based standard or documents it + * in machine-readable form: the interface's ports, items and attributes are that + * documentation, and a StandardConformance names the standards. + * The `#modularSystemInterface` keyword declares the binary case, which is the usual one; + * an interface with more than two ends specializes this definition directly. */ + attribute characteristics : InterfaceCharacteristicKind[0..*]; + } + + abstract interface modularSystemInterfaces : ModularSystemInterface[0..*] nonunique :> interfaces; + + interface def BinaryModularSystemInterface :> ModularSystemInterface, BinaryInterface { + doc /* A BinaryModularSystemInterface is a modular system interface between two ports. */ + } + + abstract interface binaryModularSystemInterfaces : BinaryModularSystemInterface[0..*] nonunique :> modularSystemInterfaces, binaryInterfaces; + + /* Open standards and conformance to them */ + + item def Standard :> Item { + doc /* A Standard is a published specification an interface, module or data item can be verified + * against. Where the standard's text is held in a standards registry or interface repository, + * registry holds a machine-readable reference to that entry. */ + attribute organization : String[0..1]; + attribute identifier : String[0..1]; + attribute version : String[0..1]; + attribute registry : String[0..1]; + } + + abstract item standards : Standard[0..*] nonunique :> items; + + item def ConsensusStandard :> Standard { + doc /* A ConsensusStandard is a widely supported, consensus-based standard — an open standard in + * the guidebook's terms — developed and maintained by a recognised standards body with the + * participation of the market its interfaces are competed in. Verification against one is + * what the statute asks of a modular system interface. */ + } + + abstract item consensusStandards : ConsensusStandard[0..*] nonunique :> standards; + + ref conformingElements[*] { + doc /* conformingElements are the interfaces, modules or items that conform in a StandardConformance. */ + } + + ref conformedStandards : Standard[*] { + doc /* conformedStandards are the standards conformed to in a StandardConformance. */ + } + + abstract connection def StandardConformance { + doc /* A StandardConformance states that its conforming elements are verified against its standards. + * Write one as a connection with a `#conformant` end and a `#conformsTo` end: + * `#conformance connection { end #conformant ::> dataLink; end #conformsTo ::> ethernet; }`. */ + ref conformingElement[1..*] :>> conformingElements :> participant { + doc /* The elements that conform. */ + } + ref conformedStandard[1..*] :>> conformedStandards :> participant { + doc /* The standards they conform to. */ + } + } + + abstract connection standardConformances : StandardConformance[0..*] nonunique { + doc /* standardConformances is the base feature for StandardConformance connection usages. */ + } + + /* Openness: data rights and proprietary elements */ + + enum def DataRightsKind { + doc /* The license categories for technical data and computer software the Defense Federal + * Acquisition Regulation Supplement provides for, which decide whether a component's technical + * data can be used to compete, upgrade or replace it. */ + unlimited; + governmentPurpose; + limited; + restricted; + smallBusinessInnovationResearch; + speciallyNegotiated; + } + + metadata def DataRights { + doc /* DataRights records the rights the acquiring organisation holds in the technical data and + * software of the annotated element, who asserted any restriction, and the basis of the assertion. */ + attribute kind : DataRightsKind; + attribute asserter : String[0..1]; + attribute basis : String[0..1]; + } + + metadata def Proprietary { + doc /* Proprietary marks an element — an interface, module, standard or data item — that a vendor + * controls rather than a consensus standard, with the rationale MOSA asks for keeping it so. */ + attribute owner : String[0..1]; + attribute rationale : String[0..1]; + } + + metadata def InterfaceControl { + doc /* InterfaceControl records the authority that controls changes to the annotated interface + * and the interface control document, if any, that publishes it. */ + attribute authority : String; + attribute document : String[0..1]; + } + + /* Requirements the strategy states */ + + requirement def MOSAObjective :> RequirementCheck { + doc /* A MOSAObjective states what the program expects its modular open systems approach to + * achieve — competition, incremental upgrade, interoperability, reuse or reduced life-cycle cost — + * with the measures that show it did. Modularity and interface requirements derive from it. */ + } + + abstract requirement mosaObjectives : MOSAObjective[0..*] nonunique :> requirementChecks; + + requirement def ModularityRequirement :> RequirementCheck { + doc /* A ModularityRequirement states how the system partitions into modules: which components are + * severable, what may be replaced independently of what, and the coupling permitted between them. */ + } + + abstract requirement modularityRequirements : ModularityRequirement[0..*] nonunique :> requirementChecks; + + requirement def InterfaceRequirement :> RequirementCheck { + doc /* An InterfaceRequirement states what a modular system interface must provide, the standard + * it must conform to, or the documentation and rights that must accompany it. */ + } + + abstract requirement interfaceRequirements : InterfaceRequirement[0..*] nonunique :> requirementChecks; + + /* Conformance assessment */ + + enum def MOSAConformanceCriterion { + doc /* The criteria a MOSA conformance assessment judges a program against, after the assessment + * criteria of the MOSA Implementation Guidebook. */ + objectives; + keyInterfaces; + standardsCompliance; + modules; + modularity; + openness; + lifeCycleCharacteristics; + conformance; + } + + metadata def MOSAConformance { + doc /* MOSAConformance records the assessed status of the annotated element or package against one + * conformance criterion, with the evidence the assessment rests on. */ + attribute criterion : MOSAConformanceCriterion; + attribute status : StatusKind; + attribute evidence : String[0..1]; + } + + /* Package template: MOSAPackage records which pillar of the approach a package serves. */ + + enum def MOSAPillar { + doc /* The five pillars of the MOSA Implementation Guidebook. */ + modularDesign; + keyInterfaces; + openStandards; + conformance; + enablingEnvironment; + } + + metadata def MOSAPackage { + doc /* Classifies a package by the MOSA pillar whose artefacts it holds. */ + :> annotatedElement : SysML::Package; + attribute pillar : MOSAPillar; + attribute description : String[0..1]; + } + + /* Viewpoints and views: the approach's products, as view conditions over its keywords */ + + concern def ModularityConcern { + doc /* How the system partitions into a platform, major system components and modular systems that + * can be separated, replaced and recombined. */ + } + + concern def InterfaceOpennessConcern { + doc /* Which boundaries between modules are designated modular system interfaces, and whether each is + * verified against a consensus standard or documented and controlled in the open. */ + } + + concern def TechnicalDataRightsConcern { + doc /* Whether the rights held in each component's technical data let it be competed, upgraded or + * replaced, and which elements remain proprietary and why. */ + } + + concern def ConformanceConcern { + doc /* Whether the program's objectives, modules, interfaces and standards meet the conformance + * criteria of the approach. */ + } + + viewpoint def ModularDesignViewpoint { + doc /* The modular structure of the system, as the architect and the program office see it. */ + frame concern : ModularityConcern; + } + + viewpoint def ModularInterfaceViewpoint { + doc /* The modular system interfaces and the standards they conform to, as an interface control + * working group sees them. */ + frame concern : InterfaceOpennessConcern; + } + + viewpoint def TechnicalDataViewpoint { + doc /* Data rights and proprietary elements, as the contracting officer and the competition + * planner see them. */ + frame concern : TechnicalDataRightsConcern; + } + + viewpoint def ConformanceViewpoint { + doc /* The MOSA requirements and the assessed conformance against them. */ + frame concern : ConformanceConcern; + } + + view def ModularDecompositionView :> BrowserView { + doc /* Tree of the major system platform, its major system components and their modular systems. */ + satisfy requirement modularDesignViewpoint : ModularDesignViewpoint; + filter @MajorSystemPlatformMetadata or @MajorSystemComponentMetadata or @ModularSystemMetadata; + render asTreeDiagram; + } + + view def ModularInterfaceView :> InterconnectionView { + doc /* Interconnection diagram of the major system components and modular systems with the modular + * system interfaces between them. */ + satisfy requirement modularInterfaceViewpoint : ModularInterfaceViewpoint; + filter @MajorSystemPlatformMetadata or @MajorSystemComponentMetadata or @ModularSystemMetadata or @ModularSystemInterfaceMetadata; + render asInterconnectionDiagram; + } + + view def InterfaceRegisterView :> GridView { + doc /* Table of the modular system interfaces. */ + satisfy requirement modularInterfaceViewpoint : ModularInterfaceViewpoint; + filter @ModularSystemInterfaceMetadata; + render asElementTable; + } + + view def StandardsView :> GridView { + doc /* Table of the standards the model names, consensus-based or not. */ + satisfy requirement modularInterfaceViewpoint : ModularInterfaceViewpoint; + filter @StandardMetadata or @ConsensusStandardMetadata; + render asElementTable; + } + + view def ProprietaryElementsView :> GridView { + doc /* Table of the elements marked proprietary. */ + satisfy requirement technicalDataViewpoint : TechnicalDataViewpoint; + filter @Proprietary; + render asElementTable; + } + + view def DataRightsView :> GridView { + doc /* Table of the elements whose data rights are recorded. */ + satisfy requirement technicalDataViewpoint : TechnicalDataViewpoint; + filter @DataRights; + render asElementTable; + } + + view def MOSARequirementsView :> GridView { + doc /* Table of the MOSA objectives and the modularity and interface requirements. */ + satisfy requirement conformanceViewpoint : ConformanceViewpoint; + filter @MOSAObjectiveMetadata or @ModularityRequirementMetadata or @InterfaceRequirementMetadata; + render asElementTable; + } + + view def ConformanceView :> GridView { + doc /* Table of the elements carrying a conformance assessment. */ + satisfy requirement conformanceViewpoint : ConformanceViewpoint; + filter @MOSAConformance; + render asElementTable; + } + + /* Document queries: the registers an interface control document tabulates. Each walks depth + * ownership levels below root and omits what lies deeper; a document binds root and, if need be, depth. */ + + calc def ModularSystemInterfaces :> Query { + doc /* The modular system interfaces declared below root: every interface definition or usage + * that conforms to ModularSystemInterface, whether it carries the keyword itself or is typed by + * a definition that does. */ + in root : Element; + in depth : Integer default 16; + WhereType( + source = Descendants(source = root, maxDepth = depth), + type = "MOSA::ModularSystemInterface" + ) + } + + calc def InterfaceRegister :> Query { + doc /* One row per modular system interface below root: name, type, characteristics and documentation. */ + in root : Element; + in depth : Integer default 16; + Project( + source = ModularSystemInterfaces(root = root, depth = depth), + properties = ("name", "type", "characteristics", "documentation") + ) + } + + calc def Standards :> Query { + doc /* The standards declared below root: every item that conforms to Standard. */ + in root : Element; + in depth : Integer default 16; + WhereType( + source = Descendants(source = root, maxDepth = depth), + type = "MOSA::Standard" + ) + } + + calc def StandardsRegister :> Query { + doc /* One row per standard below root: name, organisation, identifier, version and registry entry. */ + in root : Element; + in depth : Integer default 16; + Project( + source = Standards(root = root, depth = depth), + properties = ("name", "organization", "identifier", "version", "registry") + ) + } + + calc def ProprietaryElements :> Query { + doc /* The elements below root marked proprietary. */ + in root : Element; + in depth : Integer default 16; + WhereMetadata( + source = Descendants(source = root, maxDepth = depth), + 'metadata' = "MOSA::Proprietary" + ) + } + + calc def ProprietaryRegister :> Query { + doc /* One row per proprietary element below root: name, type and documentation. */ + in root : Element; + in depth : Integer default 16; + Project( + source = ProprietaryElements(root = root, depth = depth), + properties = ("name", "type", "documentation") + ) + } + + calc def DataRightsRegister :> Query { + doc /* One row per element below root whose data rights are recorded: name, type and documentation. */ + in root : Element; + in depth : Integer default 16; + Project( + source = WhereMetadata( + source = Descendants(source = root, maxDepth = depth), + 'metadata' = "MOSA::DataRights" + ), + properties = ("name", "type", "documentation") + ) + } + + /* Interface control document: a document definition to specialize, with the registers above as its tables */ + + abstract part def InterfaceControlDocument :> Document { + doc /* An InterfaceControlDocument publishes the modular system interfaces of a system, the + * standards they conform to and the rights held in them, as MOSA asks of a program's interface + * documentation. Specialize it and add sections whose tables run InterfaceRegister, + * StandardsRegister, DataRightsRegister and ProprietaryRegister over the system's root: + * `part def VehicleICD :> InterfaceControlDocument { part interfaces : Section { part register : Table { + * calc rows : InterfaceRegister { in :>> root = vehicle; } } } }`. */ + :>> title default = "Interface Control Document"; + } + + /* Semantic metadata: the user-defined keywords of the approach */ + + metadata def MajorSystemPlatformMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = majorSystemPlatforms meta SysML::Usage; + } + + metadata def MajorSystemComponentMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = majorSystemComponents meta SysML::Usage; + } + + metadata def ModularSystemMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = modularSystems meta SysML::Usage; + } + + metadata def ModularSystemInterfaceMetadata :> SemanticMetadata { + :> annotatedElement : SysML::InterfaceDefinition; + :> annotatedElement : SysML::InterfaceUsage; + :>> baseType = binaryModularSystemInterfaces meta SysML::Usage; + } + + metadata def KeyInterfaceMetadata :> ModularSystemInterfaceMetadata { + doc /* KeyInterfaceMetadata is the practitioner's name for a modular system interface the program + * has chosen to manage: "key interface" is not a statutory term, so a key interface is a modular + * system interface first, and every view and check treats it as one. */ + } + + metadata def StandardMetadata :> SemanticMetadata { + :> annotatedElement : SysML::ItemDefinition; + :> annotatedElement : SysML::ItemUsage; + :>> baseType default = standards meta SysML::Usage; + } + + metadata def ConsensusStandardMetadata :> StandardMetadata { + :>> baseType = consensusStandards meta SysML::Usage; + } + + metadata def ConformantMetadata :> SemanticMetadata { + :> annotatedElement : SysML::Usage; + :>> baseType = conformingElements meta SysML::Usage; + } + + metadata def ConformedStandardMetadata :> SemanticMetadata { + :> annotatedElement : SysML::Usage; + :>> baseType = conformedStandards meta SysML::Usage; + } + + metadata def StandardConformanceMetadata :> SemanticMetadata { + :> annotatedElement : SysML::ConnectionDefinition; + :> annotatedElement : SysML::ConnectionUsage; + :>> baseType = standardConformances meta SysML::Usage; + } + + metadata def MOSAObjectiveMetadata :> SemanticMetadata { + :> annotatedElement : SysML::RequirementDefinition; + :> annotatedElement : SysML::RequirementUsage; + :>> baseType = mosaObjectives meta SysML::Usage; + } + + metadata def ModularityRequirementMetadata :> SemanticMetadata { + :> annotatedElement : SysML::RequirementDefinition; + :> annotatedElement : SysML::RequirementUsage; + :>> baseType = modularityRequirements meta SysML::Usage; + } + + metadata def InterfaceRequirementMetadata :> SemanticMetadata { + :> annotatedElement : SysML::RequirementDefinition; + :> annotatedElement : SysML::RequirementUsage; + :>> baseType = interfaceRequirements meta SysML::Usage; + } +} diff --git a/libraries/MigrationMetadata.sysml b/libraries/MigrationMetadata.sysml new file mode 100644 index 0000000..1537f13 --- /dev/null +++ b/libraries/MigrationMetadata.sysml @@ -0,0 +1,43 @@ +standard library package MigrationMetadata { + doc /* What a migration from another modeling language had to make up + * because its source left it unstated. Non-normative OpenSysML + * extension, proposed for standardization; see the design record. */ + + metadata def SynthesizedName { + doc /* The annotated element was anonymous in the source; the + * migration named it so the notation could declare and refer to + * it. The name keys the element but is not one a picture shows. */ + } + + metadata def DeferredEvent { + doc /* The annotated state deferred the signal that types signal in + * the source: an occurrence while the state is active is kept and + * dispatched once the state exits, unless a transition out of the + * state accepts it. Written as `@DeferredEvent { ref :>> signal : Sig; }`. */ + ref signal : Base::Anything[1]; + } + + metadata def DeferredKeeper { + doc /* The annotated accept keeps the deferred signal it accepts for + * the state that defers it: the accept loop of the standard + * encoding stores each occurrence in the state's buffer rather than + * consuming it as the state's own behavior would, so the state's + * other accepts of the signal take an occurrence first. Written as + * `#DeferredKeeper action receive accept kept : Sig;`. */ + } + + metadata def LibraryNameAvoided { + doc /* The annotated top-level package was named, in the source model, + * like a standard library package, which a qualified name starting + * with it would resolve to; the migration renamed it and recorded + * the source name here and in its report. */ + attribute sourceName : ScalarValues::String; + } + + metadata def StandIn { + doc /* The annotated element stands for no element of the source: the + * migration made it up to write what the source left implicit, + * such as the join several edges into one node wait through. No + * diagram symbol stands at it. */ + } +} diff --git a/libraries/OOSEM.sysml b/libraries/OOSEM.sysml new file mode 100644 index 0000000..e0cb5a1 --- /dev/null +++ b/libraries/OOSEM.sysml @@ -0,0 +1,533 @@ +library package OOSEM { + doc + /* + * NON-NORMATIVE OpenSysML vocabulary for the Object-Oriented Systems Engineering Method (OOSEM). + * Each OOSEM artefact gets a definition specialising its native SysML type, a base usage, and a + * semantic-metadata keyword applying both at once: `#system part sat : Satellite;`. + * MOE/MOP, trade studies, derivation and causation reuse the standard domain libraries, re-exported here. + * The method's standard products are view definitions on the standard view definitions, each + * satisfying an OOSEM viewpoint; a view typed by one exposes a model and shows only that product. + */ + + private import ScalarValues::String; + private import Metaobjects::SemanticMetadata; + private import Parts::Part; + private import Parts::parts; + private import Items::Item; + private import Items::items; + private import Actions::Action; + private import Actions::actions; + private import Requirements::RequirementCheck; + private import Requirements::requirementChecks; + private import UseCases::UseCase; + private import UseCases::useCases; + private import AnalysisCases::AnalysisCase; + private import AnalysisCases::analysisCases; + private import StandardViewDefinitions::*; + private import Views::asTreeDiagram; + private import Views::asInterconnectionDiagram; + private import Views::asElementTable; + + public import ParametersOfInterestMetadata::*; + public import TradeStudies::*; + public import RequirementDerivation::*; + public import CauseAndEffect::*; + + /* Enterprise and stakeholder needs analysis */ + + part def Enterprise :> Part { + doc /* An Enterprise is the organisation, or system of systems, whose mission the system of interest + * serves. OOSEM models the as-is enterprise to find the problem and the to-be enterprise that + * includes the system of interest. */ + } + + abstract part enterprises : Enterprise[0..*] nonunique :> parts; + + part def Stakeholder :> Part { + doc /* A Stakeholder is a person, organisation or external system whose needs, concerns and measures of + * effectiveness the enterprise and system must satisfy. */ + } + + abstract part stakeholderParts : Stakeholder[0..*] nonunique :> parts; + + analysis def CausalAnalysis :> AnalysisCase { + doc /* A CausalAnalysis is the OOSEM analysis of an as-is enterprise that traces the problems it + * exhibits to their causes, so the to-be enterprise addresses causes rather than symptoms. The + * causes and effects themselves are modelled with the CauseAndEffect library re-exported here. */ + } + + abstract analysis causalAnalyses : CausalAnalysis[0..*] nonunique :> analysisCases; + + use case def EnterpriseUseCase :> UseCase { + doc /* An EnterpriseUseCase describes how the enterprise as a whole achieves a goal for its + * stakeholders. Its subject is the enterprise; the system of interest is at most one of its + * participants. */ + } + + abstract use case enterpriseUseCases : EnterpriseUseCase[0..*] nonunique :> useCases; + + /* Requirements, by the enterprise level they are stated at. */ + + requirement def StakeholderNeed :> RequirementCheck { + doc /* A StakeholderNeed states, in the stakeholder's own terms, a capability or quality the enterprise + * must provide. Mission requirements are derived from it. */ + } + + abstract requirement stakeholderNeeds : StakeholderNeed[0..*] nonunique :> requirementChecks; + + requirement def MissionRequirement :> RequirementCheck { + doc /* A MissionRequirement states what the enterprise must achieve, with its measures of + * effectiveness, to satisfy the stakeholder needs. It is the top of the derivation chain that ends + * in component requirements. */ + } + + abstract requirement missionRequirements : MissionRequirement[0..*] nonunique :> requirementChecks; + + requirement def SystemRequirement :> RequirementCheck { + doc /* A SystemRequirement states what the system of interest, treated as a black box in its context, + * must do or be. It is derived from mission requirements and use cases. */ + } + + abstract requirement systemRequirements : SystemRequirement[0..*] nonunique :> requirementChecks; + + requirement def ComponentRequirement :> RequirementCheck { + doc /* A ComponentRequirement states what a logical or physical component of the system must do or be, + * as derived from the system requirements allocated to it. */ + } + + abstract requirement componentRequirements : ComponentRequirement[0..*] nonunique :> requirementChecks; + + /* System requirements analysis: the black-box system in its context */ + + part def SystemOfInterest :> Part { + doc /* The SystemOfInterest is the system being specified and designed. In the system context it is a + * black box whose interfaces, behaviour and requirements are stated without reference to its + * internal structure. */ + } + + abstract part systemsOfInterest : SystemOfInterest[0..*] nonunique :> parts; + + part def ExternalSystem :> Part { + doc /* An ExternalSystem is a system outside the boundary of the system of interest that it exchanges + * matter, energy or information with. */ + } + + abstract part externalSystems : ExternalSystem[0..*] nonunique :> parts; + + part def User :> Part { + doc /* A User is a person, or class of persons, who interacts with the system of interest across its + * boundary: an operator, maintainer or other human actor. */ + } + + abstract part users : User[0..*] nonunique :> parts; + + part def Environment :> Part { + doc /* An Environment is the natural or induced physical environment that surrounds the system of + * interest and acts on it: terrain, weather, radiation, orbit, and so on. */ + } + + abstract part environments : Environment[0..*] nonunique :> parts; + + part def SystemContext :> Part { + doc /* A SystemContext is the composition of the system of interest with everything it interacts with. + * It is the subject of system use cases and the frame in which the system's external interfaces + * are defined. */ + + part systemOfInterest : SystemOfInterest[1] :> systemsOfInterest { + doc /* The one system of interest this context is drawn around. */ + } + + part externalSystems : ExternalSystem[0..*] :> OOSEM::externalSystems { + doc /* The external systems the system of interest interacts with. */ + } + + part users : User[0..*] :> OOSEM::users { + doc /* The users who interact with the system of interest. */ + } + + part environment : Environment[0..1] :> environments { + doc /* The physical environment the system of interest operates in. */ + } + } + + abstract part systemContexts : SystemContext[0..*] nonunique :> parts; + + use case def SystemUseCase :> UseCase { + doc /* A SystemUseCase describes a goal an actor achieves through the system of interest. Its subject + * is the system context, so its scenarios show the black-box system exchanging inputs and outputs + * with its actors. */ + } + + abstract use case systemUseCases : SystemUseCase[0..*] nonunique :> useCases; + + item def IOEntity :> Item { + doc /* An IOEntity is something that crosses the boundary of the system of interest: an item of matter, + * energy or information it receives or produces. */ + } + + abstract item ioEntities : IOEntity[0..*] nonunique :> items; + + item def Store :> Item { + doc /* A Store holds items, typically data, that persist between the actions of a scenario so that one + * action can deposit what a later one retrieves. */ + } + + abstract item stores : Store[0..*] nonunique :> items; + + /* Logical architecture */ + + part def LogicalComponent :> Part { + doc /* A LogicalComponent is a part of the logical decomposition of the system of interest: it is + * defined by the functions and interfaces allocated to it, independent of the technology that will + * realise it. */ + } + + abstract part logicalComponents : LogicalComponent[0..*] nonunique :> parts; + + action def LogicalScenario :> Action { + doc /* A LogicalScenario is a white-box realisation of a system use case scenario: the same externally + * visible behaviour, decomposed into actions performed by the logical components and the flows + * between them. */ + } + + abstract action logicalScenarios : LogicalScenario[0..*] nonunique :> actions; + + part def Node :> Part { + doc /* A Node is a locus of the system to which logical components are distributed before they are + * realised: a site, a vehicle, a rack. Nodes partition the logical architecture and define the + * interfaces between partitions. */ + } + + abstract part nodes : Node[0..*] nonunique :> parts; + + /* Physical architecture */ + + part def PhysicalComponent :> Part { + doc /* A PhysicalComponent realises one or more logical components in a chosen technology. Hardware, + * software, data and operational procedures are its kinds. */ + } + + abstract part physicalComponents : PhysicalComponent[0..*] nonunique :> parts; + + part def HardwareComponent :> PhysicalComponent { + doc /* A HardwareComponent is a physical component realised in hardware. */ + } + + abstract part hardwareComponents : HardwareComponent[0..*] nonunique :> physicalComponents; + + part def SoftwareComponent :> PhysicalComponent { + doc /* A SoftwareComponent is a physical component realised in software. */ + } + + abstract part softwareComponents : SoftwareComponent[0..*] nonunique :> physicalComponents; + + part def OperationalProcedure :> PhysicalComponent { + doc /* An OperationalProcedure is a physical component realised as a procedure that a user follows + * rather than as hardware or software. */ + } + + abstract part operationalProcedures : OperationalProcedure[0..*] nonunique :> physicalComponents; + + item def DataComponent :> Item { + doc /* A DataComponent is data the physical architecture stores or transfers, when the data itself is a + * design element rather than a value flowing through one. */ + } + + abstract item dataComponents : DataComponent[0..*] nonunique :> items; + + /* Lifecycle */ + + enum def EnterpriseStateKind { + doc /* Whether a model of the enterprise or system describes what exists today or what is proposed. */ + asIs; + toBe; + } + + metadata def AsIs { + doc /* Marks an as-is model: the enterprise or system as it exists before the system of interest is + * introduced. */ + attribute kind : EnterpriseStateKind = EnterpriseStateKind::asIs; + } + + metadata def ToBe { + doc /* Marks a to-be model: the enterprise or system as proposed. */ + attribute kind : EnterpriseStateKind = EnterpriseStateKind::toBe; + } + + /* Package template: OOSEMPackage records which method activity a package holds. */ + + enum def OOSEMPackageKind { + enterpriseModel; + stakeholderNeeds; + missionRequirements; + systemRequirements; + logicalArchitecture; + physicalArchitecture; + analysisModel; + verificationModel; + tradeStudies; + } + + metadata def OOSEMPackage { + doc /* Classifies a package by the OOSEM activity whose artefacts it holds. */ + :> annotatedElement : SysML::Package; + attribute kind : OOSEMPackageKind; + attribute description : String[0..1]; + } + + /* Aliases for the earlier OOSEM stub's MOE/MoP names; prefer #moe and #mop. */ + + package 'OOSEM Measures' { + alias MOE for ParametersOfInterestMetadata::measuresOfEffectiveness; + alias MoP for ParametersOfInterestMetadata::measuresOfPerformance; + } + + /* Viewpoints and views: the method's standard products, as view conditions over its keywords */ + + concern def StakeholderNeedsConcern { + doc /* Whose needs the enterprise serves, and what in the as-is enterprise keeps them unmet. */ + } + + concern def SystemBoundaryConcern { + doc /* Where the boundary of the system of interest lies, and what crosses it. */ + } + + concern def RequirementsTraceabilityConcern { + doc /* Whether every requirement derives from a need and is measured, satisfied and verified. */ + } + + concern def LogicalDecompositionConcern { + doc /* How the system's functions and interfaces partition into technology-independent components. */ + } + + concern def PhysicalRealizationConcern { + doc /* Which hardware, software, data and procedures realise the logical components, at which nodes. */ + } + + viewpoint def EnterpriseViewpoint { + doc /* The enterprise and stakeholder needs analysis, as the stakeholders and mission planners see it. */ + frame concern : StakeholderNeedsConcern; + } + + viewpoint def SystemContextViewpoint { + doc /* The black-box system in its context, as the system requirements analysis sees it. */ + frame concern : SystemBoundaryConcern; + } + + viewpoint def RequirementsViewpoint { + doc /* The requirement chain from stakeholder need to component requirement. */ + frame concern : RequirementsTraceabilityConcern; + } + + viewpoint def LogicalArchitectureViewpoint { + doc /* The logical decomposition and its distribution over nodes. */ + frame concern : LogicalDecompositionConcern; + } + + viewpoint def PhysicalArchitectureViewpoint { + doc /* The physical realisation of the logical architecture. */ + frame concern : PhysicalRealizationConcern; + } + + view def EnterpriseModelView :> BrowserView { + doc /* Tree of the enterprise, its stakeholders, enterprise use cases and causal analyses. */ + satisfy requirement enterpriseViewpoint : EnterpriseViewpoint; + filter @EnterpriseMetadata or @StakeholderMetadata or @EnterpriseUseCaseMetadata or @CausalAnalysisMetadata; + render asTreeDiagram; + } + + view def SystemContextView :> InterconnectionView { + doc /* Interconnection diagram of the system context: the system of interest, external systems, + * users and environment, with the connections between them. */ + satisfy requirement systemContextViewpoint : SystemContextViewpoint; + filter @SystemContextMetadata or @SystemOfInterestMetadata or @ExternalSystemMetadata or @UserMetadata or @EnvironmentMetadata; + render asInterconnectionDiagram; + } + + view def SystemUseCaseView :> BrowserView { + doc /* Tree of the system use cases and the entities that cross the system boundary. */ + satisfy requirement systemContextViewpoint : SystemContextViewpoint; + filter @SystemUseCaseMetadata or @IOEntityMetadata or @StoreMetadata; + render asTreeDiagram; + } + + view def RequirementsView :> GridView { + doc /* Table of the stakeholder needs and mission, system and component requirements. */ + satisfy requirement requirementsViewpoint : RequirementsViewpoint; + filter @StakeholderNeedMetadata or @MissionRequirementMetadata or @SystemRequirementMetadata or @ComponentRequirementMetadata; + render asElementTable; + } + + view def MeasuresView :> GridView { + doc /* Table of the measures of effectiveness and performance. */ + satisfy requirement requirementsViewpoint : RequirementsViewpoint; + filter @MeasureOfEffectiveness or @MeasureOfPerformance; + render asElementTable; + } + + view def LogicalArchitectureView :> BrowserView { + doc /* Tree of the logical components and the nodes they are distributed to. */ + satisfy requirement logicalArchitectureViewpoint : LogicalArchitectureViewpoint; + filter @LogicalComponentMetadata or @NodeMetadata; + render asTreeDiagram; + } + + view def LogicalScenarioView :> ActionFlowView { + doc /* Action flow diagram of the logical scenarios. */ + satisfy requirement logicalArchitectureViewpoint : LogicalArchitectureViewpoint; + filter @LogicalScenarioMetadata; + } + + view def PhysicalArchitectureView :> BrowserView { + doc /* Tree of the nodes and the hardware, software, data and procedures they hold. */ + satisfy requirement physicalArchitectureViewpoint : PhysicalArchitectureViewpoint; + filter @NodeMetadata or @PhysicalComponentMetadata or @HardwareComponentMetadata or @SoftwareComponentMetadata or @OperationalProcedureMetadata or @DataComponentMetadata; + render asTreeDiagram; + } + + /* Semantic metadata: the user-defined keywords of the method */ + + metadata def EnterpriseMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = enterprises meta SysML::Usage; + } + + metadata def StakeholderMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = stakeholderParts meta SysML::Usage; + } + + metadata def CausalAnalysisMetadata :> SemanticMetadata { + :> annotatedElement : SysML::AnalysisCaseDefinition; + :> annotatedElement : SysML::AnalysisCaseUsage; + :>> baseType = causalAnalyses meta SysML::Usage; + } + + metadata def EnterpriseUseCaseMetadata :> SemanticMetadata { + :> annotatedElement : SysML::UseCaseDefinition; + :> annotatedElement : SysML::UseCaseUsage; + :>> baseType = enterpriseUseCases meta SysML::Usage; + } + + metadata def StakeholderNeedMetadata :> SemanticMetadata { + :> annotatedElement : SysML::RequirementDefinition; + :> annotatedElement : SysML::RequirementUsage; + :>> baseType = stakeholderNeeds meta SysML::Usage; + } + + metadata def MissionRequirementMetadata :> SemanticMetadata { + :> annotatedElement : SysML::RequirementDefinition; + :> annotatedElement : SysML::RequirementUsage; + :>> baseType = missionRequirements meta SysML::Usage; + } + + metadata def SystemRequirementMetadata :> SemanticMetadata { + :> annotatedElement : SysML::RequirementDefinition; + :> annotatedElement : SysML::RequirementUsage; + :>> baseType = systemRequirements meta SysML::Usage; + } + + metadata def ComponentRequirementMetadata :> SemanticMetadata { + :> annotatedElement : SysML::RequirementDefinition; + :> annotatedElement : SysML::RequirementUsage; + :>> baseType = componentRequirements meta SysML::Usage; + } + + metadata def SystemOfInterestMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = systemsOfInterest meta SysML::Usage; + } + + metadata def ExternalSystemMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = externalSystems meta SysML::Usage; + } + + metadata def UserMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = users meta SysML::Usage; + } + + metadata def EnvironmentMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = environments meta SysML::Usage; + } + + metadata def SystemContextMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = systemContexts meta SysML::Usage; + } + + metadata def SystemUseCaseMetadata :> SemanticMetadata { + :> annotatedElement : SysML::UseCaseDefinition; + :> annotatedElement : SysML::UseCaseUsage; + :>> baseType = systemUseCases meta SysML::Usage; + } + + metadata def IOEntityMetadata :> SemanticMetadata { + :> annotatedElement : SysML::ItemDefinition; + :> annotatedElement : SysML::ItemUsage; + :>> baseType = ioEntities meta SysML::Usage; + } + + metadata def StoreMetadata :> SemanticMetadata { + :> annotatedElement : SysML::ItemDefinition; + :> annotatedElement : SysML::ItemUsage; + :>> baseType = stores meta SysML::Usage; + } + + metadata def LogicalComponentMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = logicalComponents meta SysML::Usage; + } + + metadata def LogicalScenarioMetadata :> SemanticMetadata { + :> annotatedElement : SysML::ActionDefinition; + :> annotatedElement : SysML::ActionUsage; + :>> baseType = logicalScenarios meta SysML::Usage; + } + + metadata def NodeMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = nodes meta SysML::Usage; + } + + metadata def PhysicalComponentMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = physicalComponents meta SysML::Usage; + } + + metadata def HardwareComponentMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = hardwareComponents meta SysML::Usage; + } + + metadata def SoftwareComponentMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = softwareComponents meta SysML::Usage; + } + + metadata def OperationalProcedureMetadata :> SemanticMetadata { + :> annotatedElement : SysML::PartDefinition; + :> annotatedElement : SysML::PartUsage; + :>> baseType = operationalProcedures meta SysML::Usage; + } + + metadata def DataComponentMetadata :> SemanticMetadata { + :> annotatedElement : SysML::ItemDefinition; + :> annotatedElement : SysML::ItemUsage; + :>> baseType = dataComponents meta SysML::Usage; + } +} diff --git a/libraries/OpenSysMLMathFunctions.kerml b/libraries/OpenSysMLMathFunctions.kerml new file mode 100644 index 0000000..806bfd7 --- /dev/null +++ b/libraries/OpenSysMLMathFunctions.kerml @@ -0,0 +1,67 @@ +library package OpenSysMLMathFunctions { + doc + /* + * NON-NORMATIVE. This package is an OpenSysML extension, not part of the OMG KerML + * standard library: it declares the exponential, logarithmic and two-argument + * arctangent functions, the ceiling and the Integer quotient the Kernel Function + * Library omits. + * The vendored OMG files are unmodified. A model that needs these writes + * `import OpenSysMLMathFunctions::*;`. + */ + + public import ScalarValues::*; + + function exp { + doc + /* + * The exponential of x: e raised to the power x. + */ + in x: Real[1]; return : Real[1]; + } + + function ln { + doc + /* + * The natural logarithm of x, the inverse of exp. Defined for x > 0.0. + */ + in x: Real[1]; return : Real[1]; + } + + function log { + doc + /* + * The logarithm of x to the given base, ln(x) / ln(base). Defined for x > 0.0 and + * a base that is positive and not 1.0. The base is a parameter rather than + * implied, so a call states whether it means base 10, base 2 or another base. + */ + in x: Real[1]; in base: Real[1]; return : Real[1]; + } + + function atan2 { + doc + /* + * The angle in radians from the positive x axis to the point (x, y), in + * (-pi, pi]. The parameters are ordered y then x, as in IEEE 754, so the angle + * carries the quadrant that arctan(y / x) loses. Undefined at the origin. + */ + in y: Real[1]; in x: Real[1]; return : Real[1]; + } + + function ceiling { + doc + /* + * The least Integer not less than x, the counterpart of RealFunctions::floor. + * Exact for every finite x, however large; undefined for an infinite or NaN x. + */ + in x: Real[1]; return : Integer[1]; + } + + function quotient { + doc + /* + * The Integer quotient of x by y, the exact ratio truncated toward zero, so that + * quotient(x, y) * y + x % y == x for every y other than 0. Undefined for y == 0. + */ + in x: Integer[1]; in y: Integer[1]; return : Integer[1]; + } +} diff --git a/libraries/RandomFunctions.kerml b/libraries/RandomFunctions.kerml new file mode 100644 index 0000000..7d5e4c4 --- /dev/null +++ b/libraries/RandomFunctions.kerml @@ -0,0 +1,56 @@ +library package RandomFunctions { + doc + /* + * NON-NORMATIVE. This package is an OpenSysML extension, not part of the OMG KerML + * standard library: it declares functions whose result is a random draw from a stated + * distribution, which a model uses for a random duration (`accept after uniform(1, 80) [s]`) + * or a random value (`attribute d : Real = uniform(0.0, 1.0);`). The vendored OMG files + * are unmodified. A model that needs these writes `import RandomFunctions::*;`. + * + * A draw is made from the run's modeled-randomness stream, which the run's seed + * fixes: the same seed gives the same draws, and a run given no seed refuses the call + * rather than draw an unrepeatable value. Every draw is recorded in the run's witness, + * so a replay of the run makes the same draws without drawing again. + * + * The functions are over scalars: a random quantity is a draw given a unit, as in + * `uniform(1, 80) [s]`; a quantity passed as a bound is refused. + */ + + public import ScalarValues::*; + + function uniform { + doc + /* + * A draw from the continuous uniform distribution over the interval + * lo..hi, every value of which is equally likely. Requires lo <= hi. + */ + in lo: Real[1]; in hi: Real[1]; return : Real[1]; + } + + function uniformInteger { + doc + /* + * A draw from the discrete uniform distribution over the integers lo..hi, both + * inclusive, each equally likely. Requires lo <= hi. + */ + in lo: Integer[1]; in hi: Integer[1]; return : Integer[1]; + } + + function triangular { + doc + /* + * A draw from the triangular distribution over lo..hi whose density peaks at + * mode. Requires lo <= mode <= hi and lo < hi. + */ + in lo: Real[1]; in mode: Real[1]; in hi: Real[1]; return : Real[1]; + } + + function normal { + doc + /* + * A draw from the normal distribution with the given mean and standard + * deviation sd. Requires sd >= 0.0; sd = 0.0 yields the mean. + */ + in mean: Real[1]; in sd: Real[1]; return : Real[1]; + } +} diff --git a/libraries/Simulation.sysml b/libraries/Simulation.sysml new file mode 100644 index 0000000..3f2a7af --- /dev/null +++ b/libraries/Simulation.sysml @@ -0,0 +1,91 @@ +library package Simulation { + doc + /* + * NON-NORMATIVE. This package is an OpenSysML extension, not part of the OMG SysML + * standard library: it declares the metadata by which a model records how a + * behavior is meant to be run — how many runs, how random draws resolve, and the + * clock the runs are read on. The vendored OMG files are unmodified. A tool that + * does not know the extension reads the metadata as any metadata usage. + * + * A SysML v1 model migrated from a tool with its own simulation profile carries + * one Configuration per run configuration of that tool; the migration reference + * documents which tag of the tool each attribute records. + */ + + private import ScalarValues::*; + + enum def DrawPolicy { + doc + /* + * How a run resolves a RandomFunctions draw: random takes a seeded draw, + * min and max the least and greatest value of the distribution, and + * average its mean. A run is given the policy as -draws on the command + * line or %draws in the REPL; random is the default. + */ + random; + min; + max; + average; + } + + metadata def Configuration { + doc + /* + * The run configuration of the annotated action or analysis definition: + * + * action def Nominal { + * @Configuration { runs = 100; draws = DrawPolicy::max; } + * part target : Vehicle; + * perform action run ::> target.drive; + * } + * + * runs is the number of runs the configuration was made for, to pass as + * -runs; draws is the policy to pass as -draws. timeVariable, startTime, + * stepSize and timeUnit describe the clock the configuring tool ran on; + * OpenSysML's clock is the run's own, so they are recorded, not applied. + * parallelForks records whether the tool ran a fork's branches in parallel. + * Every attribute is optional: an absent one was not stated. + */ + attribute runs : Natural[0..1]; + attribute draws : DrawPolicy[0..1]; + attribute timeVariable : String[0..1]; + attribute startTime : Real[0..1]; + attribute stepSize : Real[0..1]; + attribute timeUnit : String[0..1]; + attribute parallelForks : Boolean[0..1]; + } + + abstract analysis def MonteCarlo { + doc + /* + * An analysis of repeated runs. One run performs the case's steps on a + * fresh subject and reads observed, the value the specializing case binds: + * + * analysis def 'Timer Analysis Monte Carlo' :> MonteCarlo { + * subject analysed : 'Timer Analysis'; + * perform action run ::> analysed.tick; + * attribute :>> observed : Real = analysed.t; + * return Mean : Real = mean; + * } + * + * -runs N (or the Configuration's runs) performs N such runs, each seeded + * from -seed, and then binds the statistics of the N observations: runs is + * N, mean their arithmetic mean, deviation their sample standard deviation + * (empty under two runs, so a return of it is optional) and outOfSpec the + * number of runs in which a required check of the case — an objective's + * condition or an assertion — did not hold. The case's own outputs are + * evaluated once, over the statistics. Run once, without -runs, the + * statistics stay unbound, and a return of one of them is refused. + * + * A SysML v1 model migrated from a tool with a Monte Carlo analysis pattern + * carries one specializing case per block of that pattern; the migration + * reference documents which statistic of the tool each attribute records. + */ + subject analysed; + attribute observed[0..1]; + out attribute runs : Natural[0..1]; + out attribute mean[0..1]; + out attribute deviation[0..1]; + out attribute outOfSpec : Natural[0..1]; + } +} diff --git a/libraries/StateMachines.sysml b/libraries/StateMachines.sysml new file mode 100644 index 0000000..a00ca0b --- /dev/null +++ b/libraries/StateMachines.sysml @@ -0,0 +1,71 @@ +library package StateMachines { + doc + /* + * NON-NORMATIVE OpenSysML vocabulary for state-machine notation the SysML v2 + * grammar has no production for: pseudostates. Each kind gets a definition + * specialising its native SysML type, a base usage, and a semantic-metadata + * keyword applying both at once: `#choice state pick;` declares a choice + * pseudostate. + */ + + private import Metaobjects::SemanticMetadata; + + /* Pseudostates, by the kind of transient vertex they declare. */ + + abstract state def Pseudostate { + doc /* A Pseudostate is a transient vertex of a state machine: control passes + * through it on the way between states, so a transition reaching one leaves + * it in the same step. */ + } + + state def ChoicePseudostate :> Pseudostate { + doc /* A ChoicePseudostate selects one of its outgoing transitions by the + * guard that holds where it is reached. */ + } + + state def JunctionPseudostate :> Pseudostate { + doc /* A JunctionPseudostate chains transitions together, evaluating each + * guard as the chain is taken. */ + } + + state def ShallowHistoryPseudostate :> Pseudostate { + doc /* A ShallowHistoryPseudostate re-enters the configuration its owning + * state was last in, at the depth the transition enters it. */ + } + + state def DeepHistoryPseudostate :> Pseudostate { + doc /* A DeepHistoryPseudostate re-enters the configuration its owning + * state was last in, at every depth of the nesting below it. */ + } + + abstract state choicePseudostates : ChoicePseudostate[*] nonunique; + + abstract state junctionPseudostates : JunctionPseudostate[*] nonunique; + + abstract state shallowHistoryPseudostates : ShallowHistoryPseudostate[*] nonunique; + + abstract state deepHistoryPseudostates : DeepHistoryPseudostate[*] nonunique; + + /* Semantic metadata: the keywords spelling a state machine's transient + * vertices as annotations on its members. */ + + metadata def ChoiceMetadata :> SemanticMetadata { + :> annotatedElement : SysML::StateUsage; + :>> baseType = choicePseudostates meta SysML::Usage; + } + + metadata def JunctionMetadata :> SemanticMetadata { + :> annotatedElement : SysML::StateUsage; + :>> baseType = junctionPseudostates meta SysML::Usage; + } + + metadata def ShallowHistoryMetadata :> SemanticMetadata { + :> annotatedElement : SysML::StateUsage; + :>> baseType = shallowHistoryPseudostates meta SysML::Usage; + } + + metadata def DeepHistoryMetadata :> SemanticMetadata { + :> annotatedElement : SysML::StateUsage; + :>> baseType = deepHistoryPseudostates meta SysML::Usage; + } +} diff --git a/libraries/StateSpaceIntegration.sysml b/libraries/StateSpaceIntegration.sysml new file mode 100644 index 0000000..8cd36ce --- /dev/null +++ b/libraries/StateSpaceIntegration.sysml @@ -0,0 +1,61 @@ +library package StateSpaceIntegration { + doc + /* + * NON-NORMATIVE. This package is an OpenSysML extension, not part of the OMG SysML + * standard library: it names what the runtime provides to run a model written against + * StateSpaceRepresentation by fixed time steps. The vendored OMG files are unmodified. + * + * A model specializing ContinuousStateSpaceDynamics or DiscreteStateSpaceDynamics runs + * when it also specializes FixedStepDynamics, which states the step it advances by and + * the time it stops at. Each step advances the clock the run's actions and state + * machines share, so their timed accepts and the steps interleave in order of time. + */ + + private import StateSpaceRepresentation::*; + private import ISQ::DurationValue; + private import ScalarValues::*; + + calc def Euler :> Integrate { + doc + /* + * The forward Euler method: the state advances by timeStep times the derivative + * at the start of the step. First-order accurate; its error shrinks with the step. + */ + } + + calc def RK4 :> Integrate { + doc + /* + * The classical fourth-order Runge-Kutta method: the derivative is evaluated at the + * start, twice at the midpoint and at the end of the step, and the state advances by + * their weighted mean. The default integrator where a model names none. + */ + } + + abstract action def FixedStepDynamics :> StateSpaceDynamics { + doc + /* + * A dynamics run by fixed time steps: from the clock's time when it starts, each + * step advances the state and the clock by timeStep, until the clock reaches + * stopTime. Without a stopTime the run continues while the clock is advanced. + * time is the clock's instant at the latest step, for a guard to read; while an + * integrator evaluates getDerivative it is the instant of that stage of the step. + */ + attribute timeStep : DurationValue; + attribute stopTime : DurationValue[0..1]; + attribute time : DurationValue; + } + + action def ZeroCrossing :> ZeroCrossingEventDef { + doc + /* + * A zero crossing of guard, a real-valued expression over the state, the input + * and the time, watched between consecutive steps: the event occurs when the guard's + * sign after a step differs from its sign before it, or the guard is exactly + * zero after a step. The crossing is located at the end of the step that + * detected it. A terminal crossing ends the dynamics at that step. + */ + attribute guard : Real; + attribute terminal : Boolean default false; + } +} diff --git a/libraries/Stochastic.sysml b/libraries/Stochastic.sysml new file mode 100644 index 0000000..d4daedc --- /dev/null +++ b/libraries/Stochastic.sysml @@ -0,0 +1,36 @@ +library package Stochastic { + doc + /* + * NON-NORMATIVE. This package is an OpenSysML extension, not part of the OMG SysML + * standard library: it declares the metadata by which a model states the probability + * of a branch, which the SysML v2 notation has no other way to say. The vendored OMG + * files are unmodified. A tool that does not know the extension reads the metadata as + * any metadata usage and executes the branches as an unweighted choice. + * + * Random draws that a branch's probability or a random duration is decided by come + * from the RandomFunctions library, and are made only in a run given a seed. + */ + + private import ScalarValues::Real; + + metadata def Probability { + doc + /* + * The probability that a run takes the annotated succession out of a decision + * node, among the alternatives that hold at that moment. It annotates the + * succession from within its body: + * + * decide d; + * first d then fast { @Probability { p = 0.7; } } + * first d then slow { @Probability { p = 0.3; } } + * + * Either every succession out of one decision node carries a Probability or + * none does; p lies in 0.0..1.0; and where every p out of one node is a constant + * they sum to 1.0. A seeded run draws a branch by these weights, an unseeded + * run takes the most probable, and a weighted branch whose guard does not hold + * is left out of the draw, the others' weights renormalized. A transition out + * of a state cannot be weighted: one carrying a Probability is refused. + */ + attribute p : Real; + } +} diff --git a/libraries/SysMLValidation.sysml b/libraries/SysMLValidation.sysml new file mode 100644 index 0000000..493eed0 --- /dev/null +++ b/libraries/SysMLValidation.sysml @@ -0,0 +1,107 @@ +standard library package SysMLValidation { + doc + /* + * Validation constraints of the KerML and SysML abstract syntax, stated over + * the reflective metamodel (KerML, SysML). Each constraint definition is + * named for the specification constraint it states and takes the element it + * checks as its one input, typed by the metaclass the specification declares + * the constraint on; it applies to every element that metaclass classifies. + * Constraints whose OCL reads metaclass features the reflective model does + * not yet derive are not stated here. + */ + + private import ControlFunctions::*; + + /* KerML: Feature */ + + constraint def validateFeaturePortionNotVariable { + doc /* isPortion implies not isVariable */ + in element : KerML::Feature; + element.isPortion implies not element.isVariable + } + + constraint def validateFeatureConstantIsVariable { + doc /* isConstant implies isVariable */ + in element : KerML::Feature; + element.isConstant implies element.isVariable + } + + constraint def validateFeatureEndNotDerivedAbstractCompositeOrPortion { + doc /* isEnd implies not (isDerived or isAbstract or isComposite or isPortion) */ + in element : KerML::Feature; + element.isEnd implies + not (element.isDerived or element.isAbstract or element.isComposite or element.isPortion) + } + + constraint def validateFeatureEndIsConstant { + doc /* isEnd and isVariable implies isConstant */ + in element : KerML::Feature; + element.isEnd and element.isVariable implies element.isConstant + } + + /* SysML: Attributes */ + + constraint def validateAttributeUsageIsReference { + doc /* isReference */ + in element : SysML::AttributeUsage; + element.isReference + } + + constraint def validateAttributeUsageFeatures { + doc /* feature->forAll(not isComposite) */ + in element : SysML::AttributeUsage; + element.feature->forAll { in f : KerML::Feature; not f.isComposite } + } + + constraint def validateAttributeDefinitionFeatures { + doc /* feature->forAll(not isComposite) */ + in element : SysML::AttributeDefinition; + element.feature->forAll { in f : KerML::Feature; not f.isComposite } + } + + /* SysML: Enumerations */ + + constraint def validateEnumerationDefinitionIsVariation { + doc /* isVariation */ + in element : SysML::EnumerationDefinition; + element.isVariation + } + + /* SysML: References and occurrences */ + + constraint def validateReferenceUsageIsReference { + doc /* isReference */ + in element : SysML::ReferenceUsage; + element.isReference + } + + constraint def validateEventOccurrenceUsageIsReference { + doc /* isReference */ + in element : SysML::EventOccurrenceUsage; + element.isReference + } + + /* SysML: Actions */ + + constraint def validateControlNodeIsComposite { + doc /* isComposite */ + in element : SysML::ControlNode; + element.isComposite + } + + /* SysML: Ports */ + + constraint def validatePortDefinitionOwnedUsagesNotComposite { + doc /* ownedUsage->reject(oclIsKindOf(PortUsage))->forAll(not isComposite) */ + in element : SysML::PortDefinition; + element.ownedUsage->reject { in u : SysML::Usage; u istype SysML::PortUsage } + ->forAll { in u : SysML::Usage; not u.isComposite } + } + + constraint def validatePortUsageNestedUsagesNotComposite { + doc /* nestedUsage->reject(oclIsKindOf(PortUsage))->forAll(not isComposite) */ + in element : SysML::PortUsage; + element.nestedUsage->reject { in u : SysML::Usage; u istype SysML::PortUsage } + ->forAll { in u : SysML::Usage; not u.isComposite } + } +} diff --git a/scripts/build-kpar.py b/scripts/build-kpar.py new file mode 100755 index 0000000..21d4279 --- /dev/null +++ b/scripts/build-kpar.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Build a KerML 10.3 project archive (.kpar) of libraries/. + +The archive is a ZIP holding the textual model units plus two manifests: +.project.json (name, version, license, website, usage) and .meta.json +(root-namespace index, metamodel URI, creation time, SHA-256 checksums) — +the shape sysml-toolkit and the normative SysML-v2-Release archives use: +deflate entries, zeroed DOS dates, sorted entries, so the output is +byte-deterministic for a given tree. + +Usage: build-kpar.py --version X.Y.Z --output PATH +""" + +import argparse +import hashlib +import json +import re +import subprocess +import sys +import zipfile +from pathlib import Path + +METAMODEL = "https://www.omg.org/spec/SysML/20250201" +PROJECT_NAME = "OpenSysML Extension Libraries" +WEBSITE = "https://github.com/Open-MBEE/OpenSysML-Extensions-Library" +DESCRIPTION = ( + "Community non-normative SysML v2 and KerML extension libraries, " + "maintained in the OpenSysML-Extensions-Library repository." +) + +PACKAGE_RE = re.compile(r"^(?:standard\s+)?library\s+package\s+([A-Za-z_][A-Za-z0-9_]*)", re.M) + + +def top_level_package(source: str, stem: str) -> str: + match = PACKAGE_RE.search(source) + return match.group(1) if match else stem + + +def head_commit_time(repo_root: Path) -> str: + out = subprocess.run( + ["git", "-C", str(repo_root), "log", "-1", "--format=%cI", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + # Normalize to UTC ISO-8601 for reproducibility across build machines. + from datetime import datetime, timezone + + return datetime.fromisoformat(out).astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--version", required=True, help="archive version, e.g. 1.2.0") + parser.add_argument("--output", required=True, help="path of the .kpar to write") + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parent.parent + libraries = repo_root / "libraries" + unit_paths = sorted( + p for p in libraries.iterdir() if p.suffix in (".sysml", ".kerml") and p.is_file() + ) + if not unit_paths: + print(f"error: {libraries} holds no .sysml or .kerml file", file=sys.stderr) + return 1 + + units = [] # (unit path, source bytes, root package name) + for path in unit_paths: + source = path.read_bytes() + text = source.decode("utf-8") + units.append((path.name, source, top_level_package(text, path.stem))) + + project = { + "name": PROJECT_NAME, + "version": args.version, + "description": DESCRIPTION, + "license": "Apache-2.0", + "website": WEBSITE, + "usage": [], + } + meta = { + "index": {root: name for name, _, root in units}, + "created": head_commit_time(repo_root), + "metamodel": METAMODEL, + "checksum": { + name: {"value": hashlib.sha256(data).hexdigest(), "algorithm": "SHA256"} + for name, data, _ in units + }, + } + + entries = [ + (".project.json", json.dumps(project, separators=(",", ":")).encode()), + (".meta.json", json.dumps(meta, separators=(",", ":")).encode()), + ] + [(name, data) for name, data, _ in units] + entries.sort(key=lambda e: e[0]) + + # The normative archives' fixed timestamp: 1980-01-01 00:00. + epoch = (1980, 1, 1, 0, 0, 0) + output = Path(args.output) + with zipfile.ZipFile(output, "w") as archive: + for name, data in entries: + info = zipfile.ZipInfo(name, date_time=epoch) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0 + info.create_system = 0 + archive.writestr(info, data) + + print(f"Wrote {output} ({len(units)} units, version {args.version})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check-engine-contract.py b/scripts/check-engine-contract.py new file mode 100755 index 0000000..135d37b --- /dev/null +++ b/scripts/check-engine-contract.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Validate engine-contract.json and write the flat TSV the Java checker reads. + +Each line is `name TAB kind TAB comma-separated-parameters-or-attributes` +(the third field is empty for package, document and element entries). + +Usage: check-engine-contract.py [MANIFEST] > entries.tsv +""" + +import json +import sys +from pathlib import Path + +KINDS = {"package", "function", "metadata", "document", "element"} +QUALIFIED_NAME = __import__("re").compile(r"[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*") + + +def fail(message: str) -> None: + print(f"error: {message}", file=sys.stderr) + sys.exit(1) + + +def main() -> int: + path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parent.parent / "engine-contract.json" + try: + manifest = json.loads(path.read_text()) + except (OSError, json.JSONDecodeError) as e: + fail(f"{path}: {e}") + + if not isinstance(manifest.get("contract"), int) or isinstance(manifest["contract"], bool): + fail("contract must be an integer") + entries = manifest.get("entries") + if not isinstance(entries, list): + fail("entries must be a list") + + names = [] + for index, entry in enumerate(entries): + if not isinstance(entry, dict): + fail(f"entries[{index}] is not an object") + name = entry.get("name") + if not isinstance(name, str) or not QUALIFIED_NAME.fullmatch(name): + fail(f"entries[{index}]: bad name {name!r}") + kind = entry.get("kind") + if kind not in KINDS: + fail(f"{name}: unknown kind {kind!r}") + if kind == "function": + parameters = entry.get("parameters") + if not isinstance(parameters, list) or not all(isinstance(p, str) for p in parameters): + fail(f"{name}: function entries require a parameters list of names") + elif kind == "metadata": + attributes = entry.get("attributes") + if not isinstance(attributes, list) or not all(isinstance(a, str) for a in attributes): + fail(f"{name}: metadata entries require an attributes list of names") + consumers = entry.get("consumers", []) + if not isinstance(consumers, list) or not all(isinstance(c, str) for c in consumers): + fail(f"{name}: consumers must be a list of repo-relative paths") + names.append(name) + + if len(set(names)) != len(names): + duplicates = sorted({n for n in names if names.count(n) > 1}) + fail(f"duplicate entries: {', '.join(duplicates)}") + if names != sorted(names): + fail("entries are not sorted by name") + + for entry in entries: + spec = entry.get("parameters", entry.get("attributes", [])) or [] + print(f"{entry['name']}\t{entry['kind']}\t{','.join(spec)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check-engine-contract.sh b/scripts/check-engine-contract.sh new file mode 100755 index 0000000..e387b74 --- /dev/null +++ b/scripts/check-engine-contract.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Check engine-contract.json: schema and sorted/unique entries (Python side), +# resolution and kind/parameter/attribute conformance against the pinned pilot +# (Java side). With --base REF, also diff against the manifest at REF and +# classify breaking changes; a breaking change requires `contract` to be +# bumped, which then means the next release must be a MAJOR version and an +# OpenSysML PR must re-pin its vendored copy. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +manifest="$repo_root/engine-contract.json" +libraries="$repo_root/libraries" +checker="$repo_root/build/pilot-sysml-validator/check-engine-contract" +sysml_library="$repo_root/build/pilot-validator/target/sysml-download/sysml/sysml.library" + +base="" +while [[ $# -gt 0 ]]; do + case "$1" in + --base) + base="${2:?--base needs a ref}" + shift 2 + ;; + *) + echo "error: unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +tsv="$(mktemp)" +trap 'rm -f "$tsv" "$tsv.py.err"' EXIT + +if ! python3 "$repo_root/scripts/check-engine-contract.py" "$manifest" >"$tsv" 2>"$tsv.py.err"; then + cat "$tsv.py.err" >&2 + exit 1 +fi + +"$repo_root/scripts/download-pilot-sysml-validator.sh" >/dev/null +if [[ ! -x "$checker" ]]; then + echo "error: contract checker not found at $checker" >&2 + exit 1 +fi +"$checker" "$sysml_library" "$libraries" "$tsv" + +if [[ -z "$base" ]]; then + exit 0 +fi + +if ! base_manifest="$(git -C "$repo_root" show "$base:engine-contract.json" 2>/dev/null)"; then + echo "No engine-contract.json at $base; skipping the compatibility diff." + exit 0 +fi + +base_json="$(mktemp --suffix=.json)" +trap 'rm -f "$tsv" "$tsv.py.err" "$base_json"' EXIT +printf '%s' "$base_manifest" >"$base_json" + +python3 - "$base_json" "$manifest" <<'PY' +import json +import sys + +base = json.load(open(sys.argv[1])) +head = json.load(open(sys.argv[2])) +base_entries = {e["name"]: e for e in base.get("entries", [])} +head_entries = {e["name"]: e for e in head.get("entries", [])} +base_contract = base.get("contract") +head_contract = head.get("contract") +bumped = isinstance(head_contract, int) and isinstance(base_contract, int) and head_contract > base_contract + +breaking = [] +for name, old in base_entries.items(): + new = head_entries.get(name) + if new is None: + breaking.append(f"{name}: entry removed") + continue + if new.get("kind") != old.get("kind"): + breaking.append(f"{name}: kind {old.get('kind')} -> {new.get('kind')}") + if old.get("kind") == "function" and new.get("parameters") != old.get("parameters"): + breaking.append(f"{name}: parameters {old.get('parameters')} -> {new.get('parameters')}") + if old.get("kind") == "metadata": + removed = [a for a in old.get("attributes", []) if a not in new.get("attributes", [])] + if removed: + breaking.append(f"{name}: attributes removed: {', '.join(removed)}") + +if not breaking: + sys.exit(0) + +for line in breaking: + print(f"engine-contract: breaking change: {line}", file=sys.stderr) +if bumped: + print("contract bumped " + f"{base_contract} -> {head_contract}: the next release must be a MAJOR version " + "and an OpenSysML PR must re-pin its vendored copy.") + sys.exit(0) +print("error: breaking engine-contract changes require bumping `contract`", + file=sys.stderr) +sys.exit(1) +PY diff --git a/scripts/check-readme-table.sh b/scripts/check-readme-table.sh new file mode 100755 index 0000000..fe5129b --- /dev/null +++ b/scripts/check-readme-table.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Check that README.md's library table and libraries/ agree: every file gets +# exactly one table row, every row names an existing file, and the file stem is +# the top-level package the file declares. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +libraries="$repo_root/libraries" +readme="$repo_root/README.md" + +fail=0 + +for file in "$libraries"/*.sysml "$libraries"/*.kerml; do + [[ -e "$file" ]] || continue + name="$(basename "$file")" + stem="${name%.*}" + rows="$(grep -cF "| $name |" "$readme" || true)" + if [[ "$rows" -ne 1 ]]; then + echo "error: $name has $rows README table row(s), expected exactly one" >&2 + fail=1 + fi + package="$(sed -nE 's/^(standard )?library package ([A-Za-z_][A-Za-z0-9_]*).*/\2/p' "$file" | head -1)" + if [[ "$package" != "$stem" ]]; then + echo "error: $name declares top-level package '${package:-}', expected $stem" >&2 + fail=1 + fi +done + +# Every File-column entry must name a file that exists in libraries/. +while IFS= read -r entry; do + [[ -n "$entry" ]] || continue + if [[ ! -f "$libraries/$entry" ]]; then + echo "error: README table names $entry, which is not in libraries/" >&2 + fail=1 + fi +done < <(grep -oE '\| [A-Za-z0-9_]+\.(sysml|kerml) \|' "$readme" | tr -d '| ') + +if [[ "$fail" -ne 0 ]]; then + exit 1 +fi +echo "README table matches libraries/" diff --git a/scripts/download-pilot-sysml-validator.sh b/scripts/download-pilot-sysml-validator.sh new file mode 100755 index 0000000..019b9ca --- /dev/null +++ b/scripts/download-pilot-sysml-validator.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# Build the plain-Java batch SysML validator into build/pilot-sysml-validator/. +# +# It compiles scripts/pilot-sysml-validator/ValidateSysML.java against the same +# pinned pilot jar that download-pilot-validator.sh provisions, and loads a +# whole directory of models as one resource set. +set -euo pipefail + +# shellcheck source=scripts/pilot-pin.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pilot-pin.sh" + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +sources=( + "$repo_root/scripts/pilot-sysml-validator/ValidateSysML.java" + "$repo_root/scripts/engine-contract/CheckEngineContract.java" +) +target="$repo_root/build/pilot-sysml-validator" +classes="$target/classes" +validator_target="$repo_root/build/pilot-validator/target/sysml-download/sysml" +pilot_jar="$validator_target/jupyter-sysml-kernel-${PILOT_ARTIFACT_VERSION}-all.jar" +library="$validator_target/sysml.library" +# Written when the validator build completes; the jar itself keeps its release-time mtime. +validator_stamp="$repo_root/build/pilot-validator/.pilot-pin" +launcher="$target/validate-sysml-batch" +force=0 + +case "${1:-}" in + --force) + force=1 + shift + ;; + "") + ;; + *) + echo "error: unknown option: $1 (only --force is supported)" >&2 + exit 1 + ;; +esac + +# Always delegate: its fast path is a stamp check, and a stale or re-pinned build is rebuilt. +"$repo_root/scripts/download-pilot-validator.sh" + +if [[ ! -f "$pilot_jar" ]]; then + echo "error: pilot shaded jar not found at $pilot_jar" >&2 + exit 1 +fi +if [[ ! -d "$library" ]]; then + echo "error: pilot standard library not found at $library" >&2 + exit 1 +fi +for source in "${sources[@]}"; do + if [[ ! -f "$source" ]]; then + echo "error: validator source not found at $source" >&2 + exit 1 + fi +done + +if command -v java >/dev/null 2>&1; then + java_bin="$(command -v java)" +elif [[ -x /usr/local/jdk-21/bin/java ]]; then + java_bin=/usr/local/jdk-21/bin/java +else + echo "error: Java 21+ is required to build the SysML validator" >&2 + exit 1 +fi + +if command -v javac >/dev/null 2>&1; then + javac_bin="$(command -v javac)" +elif [[ -x /usr/local/jdk-21/bin/javac ]]; then + javac_bin=/usr/local/jdk-21/bin/javac +else + echo "error: javac 21+ is required to build the SysML validator" >&2 + exit 1 +fi + +if command -v jar >/dev/null 2>&1; then + jar_bin="$(command -v jar)" +elif [[ -x /usr/local/jdk-21/bin/jar ]]; then + jar_bin=/usr/local/jdk-21/bin/jar +else + echo "error: jar is required to inspect the pilot SysML validator" >&2 + exit 1 +fi + +java_major="$("$java_bin" -version 2>&1 | sed -n '1s/.*version "\([0-9][0-9]*\).*/\1/p')" +if [[ -z "$java_major" ]] || [[ "$java_major" -lt 21 ]]; then + echo "error: the pilot implementation requires Java 21+, found: $("${java_bin}" -version 2>&1 | head -1)" >&2 + exit 1 +fi + +for class in \ + "org/omg/sysml/xtext/validation/SysMLValidator.class" \ + "org/omg/sysml/xtext/SysMLStandaloneSetup.class" \ + "org/omg/sysml/io/SysMLUtil.class"; do + if ! "$jar_bin" tf "$pilot_jar" | grep -Fqx "$class"; then + echo "error: pilot jar $pilot_jar is missing $class" >&2 + exit 1 + fi +done + +mkdir -p "$classes" +recompile=0 +for source in "${sources[@]}"; do + output_class="$classes/io/opensysml/pilot/$(basename "${source%.java}").class" + if [[ "$force" -eq 1 ]] || [[ ! -f "$output_class" ]] || + [[ "$output_class" -ot "$source" ]] || [[ "$output_class" -ot "$validator_stamp" ]]; then + recompile=1 + fi +done +if [[ "$recompile" -eq 1 ]]; then + echo "Compiling ${sources[*]} ..." + "$javac_bin" -cp "$pilot_jar" -d "$classes" "${sources[@]}" +else + echo "Validators already compiled at $classes" +fi + +# The pin the launcher reports, written from pilot-pin.sh rather than read out of the +# DeciSym wrapper's pom.xml, which download-pilot-validator.sh has already checked against it. +cat >"$target/pilot-pin.txt" <"$launcher_tmp" <<'EOF' +#!/bin/sh +set -e + +SCRIPT="$0" +while [ -L "$SCRIPT" ]; do + SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)" + SCRIPT="$(readlink "$SCRIPT")" + [ "${SCRIPT%"${SCRIPT#?}"}" != "/" ] && SCRIPT="$SCRIPT_DIR/$SCRIPT" +done +SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd)" + +CLASSES="$SCRIPT_DIR/classes" +JAR="$SCRIPT_DIR/../pilot-validator/target/sysml-download/sysml/jupyter-sysml-kernel-__PILOT_ARTIFACT_VERSION__-all.jar" +LIBRARY="$SCRIPT_DIR/../pilot-validator/target/sysml-download/sysml/sysml.library" + +if command -v java >/dev/null 2>&1; then + JAVA=java +elif [ -x /usr/local/jdk-21/bin/java ]; then + JAVA=/usr/local/jdk-21/bin/java +else + echo "Error: Java 21+ not found" >&2 + exit 1 +fi + +if [ ! -f "$JAR" ]; then + echo "Error: pilot SysML jar not found at $JAR" >&2 + exit 1 +fi + +has_library=0 +for arg in "$@"; do + [ "$arg" = "--library" ] && has_library=1 +done +if [ "$has_library" -eq 0 ]; then + if [ ! -d "$LIBRARY" ]; then + echo "Error: SysML library not found at $LIBRARY" >&2 + exit 1 + fi + LIBRARY="$(cd "$LIBRARY" && pwd)" + set -- --library "$LIBRARY" "$@" +fi + +exec "$JAVA" -cp "$CLASSES:$JAR" io.opensysml.pilot.ValidateSysML "$@" +EOF +sed "s|__PILOT_ARTIFACT_VERSION__|${PILOT_ARTIFACT_VERSION}|g" \ + "$launcher_tmp" >"${launcher_tmp}.out" +mv "${launcher_tmp}.out" "$launcher" +rm -f "$launcher_tmp" +chmod +x "$launcher" + +contract_launcher="$target/check-engine-contract" +contract_tmp="$(mktemp "${contract_launcher}.XXXXXX")" +trap 'rm -f "$contract_tmp" "${contract_tmp}.out"' EXIT +cat >"$contract_tmp" <<'EOF' +#!/bin/sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CLASSES="$SCRIPT_DIR/classes" +JAR="$SCRIPT_DIR/../pilot-validator/target/sysml-download/sysml/jupyter-sysml-kernel-__PILOT_ARTIFACT_VERSION__-all.jar" + +if command -v java >/dev/null 2>&1; then + JAVA=java +elif [ -x /usr/local/jdk-21/bin/java ]; then + JAVA=/usr/local/jdk-21/bin/java +else + echo "Error: Java 21+ not found" >&2 + exit 1 +fi + +if [ ! -f "$JAR" ]; then + echo "Error: pilot SysML jar not found at $JAR" >&2 + exit 1 +fi + +exec "$JAVA" -cp "$CLASSES:$JAR" io.opensysml.pilot.CheckEngineContract "$@" +EOF +sed "s|__PILOT_ARTIFACT_VERSION__|${PILOT_ARTIFACT_VERSION}|g" \ + "$contract_tmp" >"${contract_tmp}.out" +mv "${contract_tmp}.out" "$contract_launcher" +rm -f "$contract_tmp" +trap - EXIT +chmod +x "$contract_launcher" + +echo "Built $launcher (pilot $PILOT_TAG, $PILOT_ARTIFACT_VERSION)" +echo "Built $contract_launcher (pilot $PILOT_TAG, $PILOT_ARTIFACT_VERSION)" diff --git a/scripts/download-pilot-validator.sh b/scripts/download-pilot-validator.sh new file mode 100755 index 0000000..2e498d0 --- /dev/null +++ b/scripts/download-pilot-validator.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# Build DeciSym's sysmlv2-validator (a thin CLI over the OMG SysML v2 Pilot +# Implementation) into build/pilot-validator/, which the library gate runs. +# +# Both the wrapper commit and the pilot release are pinned; the pilot tag and +# artifact version come from scripts/pilot-pin.sh and are passed to Maven. +set -euo pipefail + +# shellcheck source=scripts/pilot-pin.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pilot-pin.sh" + +VALIDATOR_REPO="${VALIDATOR_REPO:-https://github.com/DeciSym/sysmlv2-validator.git}" +VALIDATOR_COMMIT="${VALIDATOR_COMMIT:-63abbd9fbc7851dc437d01b2dc07836b919770b8}" + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +target="$repo_root/build/pilot-validator" +# What the build contains, relative to its directory; checked on the installed +# copy for the fast path and on the staged copy before it replaces the old one. +pilot_jar_rel="target/sysml-download/sysml/jupyter-sysml-kernel-${PILOT_ARTIFACT_VERSION}-all.jar" +library_rel="target/sysml-download/sysml/sysml.library" +wrapper_jar_rel="target/sysmlv2-validator-1.0.0-SNAPSHOT.jar" +# The build records what it was made from, so a re-pin re-provisions it. +stamp_rel=".pilot-pin" +pin="$(pilot_pin) $PILOT_ARTIFACT_VERSION $VALIDATOR_COMMIT" + +# build_is_complete reports whether $1 holds every artifact of a finished build. +build_is_complete() { + local dir="$1" + [[ -x "$dir/validate-sysml" ]] && [[ -f "$dir/$wrapper_jar_rel" ]] && + [[ -f "$dir/$pilot_jar_rel" ]] && [[ -d "$dir/$library_rel" ]] +} + +pilot_recover_dir "$target" +if build_is_complete "$target" && [[ -f "$target/$stamp_rel" ]] && [[ "$(cat "$target/$stamp_rel")" == "$pin" ]]; then + echo "Pilot validator already built at $target (pilot $PILOT_TAG, $PILOT_ARTIFACT_VERSION)" + echo "Remove that directory to re-provision." + exit 0 +fi +if [[ -e "$target" ]]; then + if [[ -f "$target/$stamp_rel" ]] && [[ "$(cat "$target/$stamp_rel")" != "$pin" ]]; then + echo "Stale build at $target: built from $(cat "$target/$stamp_rel"), pin is now $pin; rebuilding." + elif [[ -f "$target/$stamp_rel" ]]; then + echo "Incomplete build at $target; rebuilding at pilot $PILOT_TAG ($PILOT_ARTIFACT_VERSION)." + else + echo "Unstamped build at $target; rebuilding at pilot $PILOT_TAG ($PILOT_ARTIFACT_VERSION)." + fi +fi + +for tool in git java mvn; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "error: $tool is required to build the pilot validator" >&2 + exit 1 + fi +done + +java_major="$(java -version 2>&1 | sed -n '1s/.*version "\([0-9][0-9]*\).*/\1/p')" +if [[ -z "$java_major" ]] || [[ "$java_major" -lt 21 ]]; then + echo "error: the pilot implementation requires Java 21+, found: $(java -version 2>&1 | head -1)" >&2 + exit 1 +fi + +# Built beside the target and swapped in whole, so a failed or interrupted run +# leaves the previous validator in place. +mkdir -p "$(dirname "$target")" +work="$(mktemp -d "$target.build.XXXXXX")" +trap 'rm -rf "$work"; pilot_recover_dir "$target"' EXIT + +echo "Cloning $VALIDATOR_REPO at $VALIDATOR_COMMIT ..." +git init --quiet "$work" +git -C "$work" remote add origin "$VALIDATOR_REPO" +git -C "$work" fetch --quiet --depth 1 origin "$VALIDATOR_COMMIT" +git -C "$work" checkout --quiet FETCH_HEAD + +# The wrapper must still select the release through these properties, or the +# overrides below are silently ignored. +for property in sysml.release.tag sysml.artifact.version; do + if ! grep -Fq "<${property}>" "$work/pom.xml" || ! grep -Fq "\${${property}}" "$work/pom.xml"; then + echo "error: $VALIDATOR_COMMIT no longer selects the pilot release through the $property property" >&2 + echo " re-pin VALIDATOR_COMMIT to a wrapper that does" >&2 + exit 1 + fi +done + +# The pilot is not on Maven Central: the setup-dependency profile downloads the +# jupyter-sysml-kernel release ZIP (jar + sysml.library) and installs the jar +# into ~/.m2, which `mvn package` then shades into the validator jar. +pin_properties=("-Dsysml.release.tag=$PILOT_TAG" "-Dsysml.artifact.version=$PILOT_ARTIFACT_VERSION") +echo "Downloading the pilot $PILOT_TAG ($PILOT_ARTIFACT_VERSION) release and building the validator ..." +(cd "$work" && + mvn -B -q "${pin_properties[@]}" -Psetup-dependency initialize && + mvn -B -q "${pin_properties[@]}" package) + +if [[ ! -d "$work/$library_rel" ]]; then + echo "error: the pilot standard library is missing from $work/$library_rel" >&2 + exit 1 +fi +if [[ ! -f "$work/$pilot_jar_rel" ]]; then + echo "error: the pilot shaded jar is missing from $work/$pilot_jar_rel" >&2 + exit 1 +fi +if ! build_is_complete "$work"; then + echo "error: the wrapper build under $work is incomplete" >&2 + exit 1 +fi +printf '%s' "$pin" >"$work/$stamp_rel" + +pilot_install_dir "$work" "$target" + +echo "Built $target/validate-sysml (pilot $PILOT_TAG, $PILOT_ARTIFACT_VERSION)" diff --git a/scripts/engine-contract/CheckEngineContract.java b/scripts/engine-contract/CheckEngineContract.java new file mode 100644 index 0000000..419befa --- /dev/null +++ b/scripts/engine-contract/CheckEngineContract.java @@ -0,0 +1,209 @@ +package io.opensysml.pilot; + +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.resource.Resource; +import org.omg.kerml.xtext.KerMLStandaloneSetup; +import org.omg.kerml.xtext.xmi.KerMLxStandaloneSetup; +import org.omg.sysml.io.SysMLUtil; +import org.omg.sysml.lang.sysml.Element; +import org.omg.sysml.lang.sysml.Feature; +import org.omg.sysml.lang.sysml.FeatureDirectionKind; +import org.omg.sysml.lang.sysml.Function; +import org.omg.sysml.lang.sysml.Metaclass; +import org.omg.sysml.lang.sysml.Namespace; +import org.omg.sysml.lang.sysml.SysMLPackage; +import org.omg.sysml.util.SysMLLibraryUtil; +import org.omg.sysml.xtext.SysMLStandaloneSetup; +import org.omg.sysml.xtext.xmi.SysMLxStandaloneSetup; + +import com.google.inject.Injector; + +/** + * Resolves every qualified name engine-contract.json records against libraries/ loaded + * with the standard library into one resource set, the way ValidateSysML loads them, and + * checks the contract kind and, for functions and metadata, the bound parameters and + * attributes. Each entry is a line of the TSV check-engine-contract.py writes: + * name TAB kind TAB comma-separated parameters or attributes. + * + *

Prints {@code engine-contract: : } per failure and exits 1 on any. + */ +public final class CheckEngineContract extends SysMLUtil { + + private static final PrintStream STDERR = + new PrintStream(new FileOutputStream(FileDescriptor.err), true, StandardCharsets.UTF_8); + + private static final String KERNEL_LIBRARIES_DIRECTORY = "Kernel Libraries"; + private static final String SYSTEMS_LIBRARY_DIRECTORY = "Systems Library"; + private static final String DOMAIN_LIBRARIES_DIRECTORY = "Domain Libraries"; + + private final List roots = new ArrayList<>(); + private int failures = 0; + + private CheckEngineContract() { + super(); + setVerbose(false); + } + + private void loadLibrary(Path library) { + SysMLLibraryUtil.setModelLibraryDirectory(library.toString()); + readAll(library.resolve(KERNEL_LIBRARIES_DIRECTORY).toString(), false, ".kerml"); + readAll(library.resolve(SYSTEMS_LIBRARY_DIRECTORY).toString(), false, ".sysml"); + readAll(library.resolve(DOMAIN_LIBRARIES_DIRECTORY).toString(), false, ".sysml"); + } + + private void readInputs(Path dir) throws IOException { + try (Stream walk = Files.walk(dir)) { + for (Path file : walk.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".sysml") || p.toString().endsWith(".kerml")) + .sorted() + .toList()) { + Resource resource = readResource(file.toString()); + for (var content : resource.getContents()) { + if (content instanceof Namespace namespace && content instanceof Element) { + roots.add(namespace); + } + } + } + } + } + + /** Resolve a qualified name by walking owned members by declared name from each root. */ + private Element resolve(String qualifiedName) { + String[] segments = qualifiedName.split("::"); + for (Namespace root : roots) { + Element element = walk(root, segments, 0); + if (element != null) { + return element; + } + } + return null; + } + + private Element walk(Namespace scope, String[] segments, int index) { + for (var member : scope.getOwnedMember()) { + // Semantic-metadata keywords bind by declared short name: `#choice` + // resolves ChoiceMetadata the way `StateMachines::choice` does. + if (member instanceof Element element + && (segments[index].equals(element.getDeclaredName()) + || segments[index].equals(element.getDeclaredShortName()))) { + if (index == segments.length - 1) { + return element; + } + if (element instanceof Namespace namespace) { + Element found = walk(namespace, segments, index + 1); + if (found != null) { + return found; + } + } + } + } + return null; + } + + private void fail(String name, String reason) { + STDERR.println("engine-contract: " + name + ": " + reason); + failures++; + } + + private void check(String name, String kind, String spec) { + Element element = resolve(name); + if (element == null) { + fail(name, "no element of that name resolves in libraries/"); + return; + } + List specList = spec.isEmpty() ? List.of() : List.of(spec.split(",")); + switch (kind) { + case "package" -> { + if (!(element instanceof Namespace)) { + fail(name, "not a Namespace: " + element.eClass().getName()); + } + } + case "function" -> { + if (!(element instanceof Function function)) { + fail(name, "not a Function: " + element.eClass().getName()); + break; + } + List parameters = new ArrayList<>(); + for (Feature feature : function.getOwnedFeature()) { + if (feature.getDirection() == FeatureDirectionKind.IN + || feature.getDirection() == FeatureDirectionKind.INOUT) { + parameters.add(feature.getDeclaredName()); + } + } + if (!parameters.equals(specList)) { + fail(name, "parameters " + parameters + " != " + specList); + } + } + case "metadata" -> { + if (!(element instanceof Metaclass type)) { + fail(name, "not a Metaclass: " + element.eClass().getName()); + break; + } + if (specList.isEmpty()) { + break; + } + List features = new ArrayList<>(); + for (Feature feature : type.getFeature()) { + features.add(feature.getDeclaredName()); + } + for (String attribute : specList) { + if (!features.contains(attribute)) { + fail(name, "no feature named " + attribute); + } + } + } + case "document", "element" -> { + // Existence is the whole check. + } + default -> fail(name, "unknown kind " + kind); + } + } + + public static void main(String[] args) { + if (args.length != 3) { + STDERR.println("usage: check-engine-contract --library DIR --inputs DIR TSV"); + STDERR.println(" arguments: "); + System.exit(2); + } + Path library = Paths.get(args[0]).toAbsolutePath().normalize(); + Path inputs = Paths.get(args[1]).toAbsolutePath().normalize(); + Path tsv = Paths.get(args[2]).toAbsolutePath().normalize(); + try { + EPackage.Registry.INSTANCE.put( + "https://www.omg.org/spec/SysML/20250201", SysMLPackage.eINSTANCE); + KerMLStandaloneSetup.doSetup(); + KerMLxStandaloneSetup.doSetup(); + SysMLxStandaloneSetup.doSetup(); + Injector injector = new SysMLStandaloneSetup().createInjectorAndDoEMFRegistration(); + injector.getInstance(org.eclipse.xtext.validation.IResourceValidator.class); + + CheckEngineContract instance = new CheckEngineContract(); + instance.loadLibrary(library); + instance.readInputs(inputs); + for (String line : Files.readAllLines(tsv, StandardCharsets.UTF_8)) { + if (line.isBlank() || line.startsWith("#")) { + continue; + } + String[] fields = line.split("\t", -1); + String spec = fields.length > 2 ? fields[2] : ""; + instance.check(fields[0], fields[1], spec); + } + System.exit(instance.failures == 0 ? 0 : 1); + } catch (Throwable e) { + STDERR.println("Error: " + e); + System.exit(3); + } + } +} diff --git a/scripts/pilot-pin.sh b/scripts/pilot-pin.sh new file mode 100755 index 0000000..04140fe --- /dev/null +++ b/scripts/pilot-pin.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Single source of the OMG SysML v2 Pilot Implementation pin, sourced by every +# script that fetches or verifies something from it: the reference validator +# the library gate runs. +# +# Kept in one file so the release under comparison cannot drift between them. +# The tag names the release; the commit is what every fetch verifies, because a +# tag is a mutable ref and the baselines record content. Change them together. +PILOT_TAG="${PILOT_TAG:-2026-08}" +PILOT_COMMIT="${PILOT_COMMIT:-692170b71867353b8f90341e61556f49a5beb0e5}" +PILOT_REPO="${PILOT_REPO:-https://github.com/Systems-Modeling/SysML-v2-Pilot-Implementation.git}" +PILOT_ARTIFACT_VERSION="${PILOT_ARTIFACT_VERSION:-0.62.0}" + +# pilot_pin is the stamp a fetched destination records. +pilot_pin() { + printf '%s %s %s' "$PILOT_TAG" "$PILOT_COMMIT" "$PILOT_REPO" + return 0 +} + +# pilot_recover_dir puts back the $1.old backup an interrupted pilot_install_dir left behind. +pilot_recover_dir() { + local dst="$1" + if [[ ! -e "$dst" ]] && [[ -e "$dst.old" ]]; then + mv "$dst.old" "$dst" + fi + return 0 +} + +# pilot_install_dir replaces directory $2 with $1, keeping the old copy as +# $2.old until the rename into place succeeds and restoring it if that fails. +pilot_install_dir() { + local src="$1" dst="$2" + mkdir -p "$(dirname "$dst")" + pilot_recover_dir "$dst" + rm -rf "$dst.new" "$dst.old" + if ! mv "$src" "$dst.new"; then + rm -rf "$dst.new" + return 1 + fi + if [[ -e "$dst" ]] && ! mv "$dst" "$dst.old"; then + rm -rf "$dst.new" + return 1 + fi + if ! mv "$dst.new" "$dst"; then + pilot_recover_dir "$dst" + rm -rf "$dst.new" + return 1 + fi + rm -rf "$dst.old" + return 0 +} diff --git a/scripts/pilot-sysml-validator/ValidateSysML.java b/scripts/pilot-sysml-validator/ValidateSysML.java new file mode 100644 index 0000000..93f41f7 --- /dev/null +++ b/scripts/pilot-sysml-validator/ValidateSysML.java @@ -0,0 +1,274 @@ +package io.opensysml.pilot; + +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.diagnostics.Severity; +import org.eclipse.xtext.resource.IResourceServiceProvider; +import org.eclipse.xtext.util.CancelIndicator; +import org.eclipse.xtext.validation.CheckMode; +import org.eclipse.xtext.validation.IResourceValidator; +import org.eclipse.xtext.validation.Issue; +import org.omg.kerml.xtext.KerMLStandaloneSetup; +import org.omg.kerml.xtext.xmi.KerMLxStandaloneSetup; +import org.omg.sysml.io.SysMLUtil; +import org.omg.sysml.lang.sysml.SysMLPackage; +import org.omg.sysml.util.SysMLLibraryUtil; +import org.omg.sysml.xtext.SysMLStandaloneSetup; +import org.omg.sysml.xtext.xmi.SysMLxStandaloneSetup; + +import com.google.inject.Injector; + +/** + * Loads .sysml and .kerml files into one resource set and validates each file with its own + * language's pilot validator; .kerml files use KerMLValidator as in ValidateKerML. + * + *

This is a genuine batch load: every file is read before any is validated, unlike the + * accumulating SysMLInteractive session the DeciSym CLI drives one file at a time. + * + *

Reports Xtext issues on stderr in GNU format: file:line:column: severity: message. + */ +public final class ValidateSysML extends SysMLUtil { + + private static final PrintStream STDERR = + new PrintStream(new FileOutputStream(FileDescriptor.err), true, StandardCharsets.UTF_8); + + private static final String KERML_EXTENSION = ".kerml"; + private static final String SYSML_EXTENSION = ".sysml"; + + private static final String LIBRARY_DIRECTORY_NAME = "sysml.library"; + private static final String KERNEL_LIBRARIES_DIRECTORY = "Kernel Libraries"; + private static final String SYSTEMS_LIBRARY_DIRECTORY = "Systems Library"; + private static final String DOMAIN_LIBRARIES_DIRECTORY = "Domain Libraries"; + + private static final Pattern OBJECT_REFERENCE = + Pattern.compile("@[0-9a-f]++\\{file:([^#{}]*+)#([^{}]*+)\\}"); + + private final IResourceValidator validator; + private final Path root; + private final List inputs = new ArrayList<>(); + private boolean hasErrors = false; + + private static final class Input { + private final Path file; + private final Resource resource; + + private Input(Path file, Resource resource) { + this.file = file; + this.resource = resource; + } + } + + private ValidateSysML(IResourceValidator validator, Path root) { + super(); + this.validator = validator; + this.root = root; + setVerbose(false); + } + + /** Load the standard library the way the pilot's own interactive session does. */ + private void loadLibrary(Path library) { + SysMLLibraryUtil.setModelLibraryDirectory(library.toString()); + readAll(library.resolve(KERNEL_LIBRARIES_DIRECTORY).toString(), false, KERML_EXTENSION); + readAll(library.resolve(SYSTEMS_LIBRARY_DIRECTORY).toString(), false, SYSML_EXTENSION); + readAll(library.resolve(DOMAIN_LIBRARIES_DIRECTORY).toString(), false, SYSML_EXTENSION); + } + + /** Read every input file into the shared resource set and the Xtext index. */ + private void readInputs(List files) { + for (Path file : files) { + try { + Resource resource = readResource(file.toString()); + addInputResource(resource); + inputs.add(new Input(file, resource)); + } catch (RuntimeException e) { + report(file, 1, 1, Severity.ERROR, message(e)); + } + } + } + + private void validateInputs() { + for (Input input : inputs) { + try { + IResourceServiceProvider resourceServiceProvider = + IResourceServiceProvider.Registry.INSTANCE + .getResourceServiceProvider(input.resource.getURI()); + IResourceValidator resourceValidator = resourceServiceProvider == null + ? validator + : resourceServiceProvider.getResourceValidator(); + for (Issue issue : resourceValidator.validate( + input.resource, CheckMode.ALL, CancelIndicator.NullImpl)) { + int line = issue.getLineNumber() == null ? 1 : issue.getLineNumber(); + int column = issue.getColumn() == null ? 1 : issue.getColumn(); + report(input.file, line, column, issue.getSeverity(), issue.getMessage()); + } + } catch (RuntimeException e) { + report(input.file, 1, 1, Severity.ERROR, message(e)); + } + } + } + + /** Print one diagnostic in GNU format, collapsing embedded newlines. */ + private void report(Path file, int line, int column, Severity severity, String message) { + String name = display(file); + String text = message == null ? "" : normalize(message.replaceAll("\\s+", " ").trim()); + STDERR.println(name + ":" + line + ":" + column + ": " + + severity.toString().toLowerCase() + ": " + text); + if (severity == Severity.ERROR) { + hasErrors = true; + } + } + + /** Drop identity hash codes and absolute URIs from EMF object references. */ + private String normalize(String message) { + Matcher matcher = OBJECT_REFERENCE.matcher(message); + StringBuilder result = new StringBuilder(); + while (matcher.find()) { + String path = URI.createURI("file:" + matcher.group(1)).toFileString(); + String name = path == null ? matcher.group(1) : display(Paths.get(path)); + String replacement = "{" + name + "#" + matcher.group(2) + "}"; + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + return result.toString(); + } + + /** Root-relative path where possible, else a library- or name-relative one. */ + private String display(Path file) { + if (root != null && file.startsWith(root)) { + return root.relativize(file).toString(); + } + int library = file.getNameCount(); + for (int i = 0; i < file.getNameCount(); i++) { + if (file.getName(i).toString().equals(LIBRARY_DIRECTORY_NAME)) { + library = i; + break; + } + } + if (library < file.getNameCount()) { + return file.subpath(library, file.getNameCount()).toString(); + } + return file.getFileName().toString(); + } + + private static String message(Throwable e) { + String text = e.getMessage(); + return text == null ? e.getClass().getName() : text; + } + + private static List collect(List arguments) throws IOException { + List files = new ArrayList<>(); + for (String argument : arguments) { + Path path = Paths.get(argument).toAbsolutePath().normalize(); + if (Files.isDirectory(path)) { + try (Stream walk = Files.walk(path)) { + walk.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(SYSML_EXTENSION) + || p.toString().endsWith(KERML_EXTENSION)) + .sorted() + .forEach(files::add); + } + } else { + files.add(path); + } + } + return files; + } + + private static void usage() { + STDERR.println("usage: validate-sysml --library DIR [--root DIR] FILE|DIR..."); + STDERR.println("FILE may end in .sysml or .kerml; DIR is walked for both."); + } + + public static void main(String[] args) { + try { + Path library = null; + Path root = null; + List inputs = new ArrayList<>(); + int index = 0; + while (index < args.length) { + String argument = args[index++]; + switch (argument) { + case "--library" -> library = value(args, index++, "--library"); + case "--root" -> root = value(args, index++, "--root"); + case "-h", "--help" -> { + usage(); + System.exit(0); + } + default -> inputs.add(argument); + } + } + + if (library == null) { + String property = System.getProperty(LIBRARY_DIRECTORY_NAME); + library = property == null ? null : Paths.get(property).toAbsolutePath().normalize(); + } + if (library == null || !Files.isDirectory(library)) { + STDERR.println("Error: SysML library not found: " + library); + usage(); + System.exit(2); + } + if (inputs.isEmpty()) { + usage(); + System.exit(2); + } + + List files = collect(inputs); + for (Path file : files) { + if (!file.toString().endsWith(SYSML_EXTENSION) + && !file.toString().endsWith(KERML_EXTENSION)) { + STDERR.println("Error: File must have .sysml or .kerml extension: " + file); + System.exit(2); + } + if (!Files.isRegularFile(file)) { + STDERR.println("Error: File not found: " + file); + System.exit(2); + } + } + if (files.isEmpty()) { + STDERR.println("Warning: No .sysml or .kerml files found"); + System.exit(0); + } + + // The same registration sequence SysMLInteractive.createInstance performs, so the + // only difference from the interactive path is how resources are loaded. + EPackage.Registry.INSTANCE.put( + "https://www.omg.org/spec/SysML/20250201", SysMLPackage.eINSTANCE); + KerMLStandaloneSetup.doSetup(); + KerMLxStandaloneSetup.doSetup(); + SysMLxStandaloneSetup.doSetup(); + Injector injector = new SysMLStandaloneSetup().createInjectorAndDoEMFRegistration(); + IResourceValidator validator = injector.getInstance(IResourceValidator.class); + + ValidateSysML instance = new ValidateSysML(validator, root); + instance.loadLibrary(library); + instance.readInputs(files); + instance.validateInputs(); + System.exit(instance.hasErrors ? 1 : 0); + } catch (Throwable e) { + STDERR.println("Error: " + message(e)); + System.exit(3); + } + } + + private static Path value(String[] args, int index, String option) { + if (index >= args.length || args[index].startsWith("--")) { + throw new IllegalArgumentException(option + " requires a value"); + } + return Paths.get(args[index]).toAbsolutePath().normalize(); + } +} diff --git a/scripts/validate-libraries.sh b/scripts/validate-libraries.sh new file mode 100755 index 0000000..f2036a9 --- /dev/null +++ b/scripts/validate-libraries.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# The library gate: run the pinned OMG pilot batch validator over libraries/ +# and require zero errors and zero warnings, except the warnings allow-listed +# in validation/allowed-warnings.txt. The allow-list is a ratchet: every entry +# must be emitted by the current run (a stale entry fails) and entries are +# only ever deleted, never added. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +libraries="$repo_root/libraries" +allowed="$repo_root/validation/allowed-warnings.txt" +validator="$repo_root/build/pilot-sysml-validator/validate-sysml-batch" +sysml_library="$repo_root/build/pilot-validator/target/sysml-download/sysml/sysml.library" + +if [[ -z "$(find "$libraries" -type f \( -name '*.sysml' -o -name '*.kerml' \) -print -quit)" ]]; then + echo "error: $libraries holds no .sysml or .kerml file" >&2 + exit 1 +fi + +"$repo_root/scripts/download-pilot-sysml-validator.sh" + +if [[ ! -x "$validator" ]]; then + echo "error: batch validator not found at $validator" >&2 + exit 1 +fi + +output="$(mktemp)" +trap 'rm -f "$output"' EXIT + +status=0 +# --root makes diagnostics name each file by its name inside libraries/, which +# is how validation/allowed-warnings.txt records them. +"$validator" --library "$sysml_library" --root "$libraries" "$libraries" >"$output" 2>&1 || status=$? +cat "$output" +if [[ "$status" -ne 0 ]]; then + echo "error: the pilot validator exited with status $status" >&2 + exit 1 +fi + +# Only file:line:col diagnostics count; log4j noise on stderr does not match. +diagnostics="$(grep -E '^[^:]+:[0-9]+:[0-9]+: (error|warning): ' "$output" || true)" + +fail=0 +if [[ -n "$diagnostics" ]]; then + while IFS= read -r line; do + if [[ "$line" == *": error: "* ]]; then + echo "error: $line" >&2 + fail=1 + elif ! grep -Fqx "$line" <(grep -v '^\s*#' "$allowed" | grep -v '^\s*$'); then + echo "error: warning not in validation/allowed-warnings.txt: $line" >&2 + fail=1 + fi + done <<<"$diagnostics" +fi + +# A stale allow-list entry — one the current run did not emit — fails too. +while IFS= read -r entry; do + [[ "$entry" =~ ^[[:space:]]*(#|$) ]] && continue + if ! grep -Fqx "$entry" <<<"$diagnostics"; then + echo "error: stale allow-list entry not emitted by this run: $entry" >&2 + fail=1 + fi +done <"$allowed" + +if [[ "$fail" -ne 0 ]]; then + exit 1 +fi + +count="$(grep -cE '^[^:]+:[0-9]+:[0-9]+: (error|warning): ' <<<"$diagnostics" || true)" +echo "Validation clean: 0 errors, $count allow-listed warning(s)" diff --git a/scripts/verify-pilot-pin.sh b/scripts/verify-pilot-pin.sh new file mode 100755 index 0000000..f931fab --- /dev/null +++ b/scripts/verify-pilot-pin.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Verify the pinned pilot tag still resolves to the pinned commit, without +# cloning anything. Fails when the tag has moved or been deleted. +set -euo pipefail + +# shellcheck source=scripts/pilot-pin.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pilot-pin.sh" + +refs="$(git ls-remote "$PILOT_REPO" "refs/tags/$PILOT_TAG" "refs/tags/$PILOT_TAG^{}")" +if [[ -z "$refs" ]]; then + echo "error: $PILOT_REPO has no tag $PILOT_TAG, which scripts/pilot-pin.sh pins" >&2 + exit 1 +fi + +# An annotated tag lists two refs: the tag object and its peeled commit. +resolved="" +while IFS=$'\t' read -r sha ref; do + if [[ "$ref" == "refs/tags/$PILOT_TAG^{}" ]]; then + resolved="$sha" + elif [[ "$ref" == "refs/tags/$PILOT_TAG" ]] && [[ -z "$resolved" ]]; then + resolved="$sha" + fi +done <<<"$refs" + +if [[ "$resolved" != "$PILOT_COMMIT" ]]; then + echo "error: $PILOT_REPO tag $PILOT_TAG resolves to $resolved, scripts/pilot-pin.sh pins $PILOT_COMMIT" >&2 + echo " the release tag has moved: investigate what changed before re-pinning the commit," >&2 + echo " or override it together with PILOT_TAG deliberately" >&2 + exit 1 +fi + +echo "Pin verified: $PILOT_REPO tag $PILOT_TAG is $PILOT_COMMIT" diff --git a/validation/allowed-warnings.txt b/validation/allowed-warnings.txt new file mode 100644 index 0000000..906bd3c --- /dev/null +++ b/validation/allowed-warnings.txt @@ -0,0 +1,11 @@ +# Warnings the pinned OMG pilot validator emits over libraries/ that the gate +# accepts for now, exactly as the validator prints them. +# +# These four files declare `standard library package`; an open +# Open-MBEE/OpenSysML pull request changes them to `library package`. Once it +# merges, the change is ported here and these entries are deleted. This list +# may only shrink: an entry the validator stops emitting fails the gate. +DiagramLayout.sysml:1:1: warning: User library packages should not be marked as standard +IdentityMetadata.sysml:1:1: warning: User library packages should not be marked as standard +MigrationMetadata.sysml:1:1: warning: User library packages should not be marked as standard +SysMLValidation.sysml:1:1: warning: User library packages should not be marked as standard