Thank you for your interest in contributing to the OpenSysML Runtime Environment and Development Kit (MDK). This document describes how to set up a development environment, the standards a change is expected to meet, and how contributions are reviewed and released.
- Go 1.26 or later
- Git
- Make (recommended for build automation)
git clone https://github.com/Open-MBEE/OpenSysML.git
cd OpenSysML
make build # builds bin/sysml, bin/sysml-lsp, bin/sysml-grpc, bin/sysml-jupyter-kernel and bin/sysml-mcp with version info
make test # runs all tests
make lint # runs staticcheck and gosec, as CI does
./scripts/download-training-examples.sh # fetch the OMG training corpus
./scripts/download-pilot-corpora.sh # fetch the three OMG pilot corpora
./scripts/download-spec-examples.sh # extract the specification examples (needs pdftotext)
./scripts/download-pilot-library-xmi.sh # fetch the pilot's XMI of the standard library
./scripts/download-pssm-suite.sh # fetch the OMG PSSM test suiteThe gates over those downloads (tests/corpus/training_examples_test.go,
tests/corpus/pilot_corpora_test.go, tests/corpus/spec_examples_test.go,
tests/identity/pilot_library_xmi_test.go, tests/corpus/pssm_migration_test.go)
skip while their corpus is absent, so run the five scripts once before trusting a local
make test; a corpus already at the pin is left alone. CI runs the scripts itself and sets
OPENSYSML_REQUIRE_TRAINING_CORPUS=1, OPENSYSML_REQUIRE_PILOT_CORPORA=1,
OPENSYSML_REQUIRE_SPEC_EXAMPLES=1, OPENSYSML_REQUIRE_PILOT_LIBRARY_XMI=1 and
OPENSYSML_REQUIRE_PSSM_SUITE=1, which make a missing corpus a failure there instead of a skip;
an environment that sets any of them must run the matching script first. See
docs/project/pilot-corpora.md,
docs/project/spec-examples.md
and docs/project/pssm-migration.md.
For an implementation-focused tour of the core pipeline and practical recipes for changing syntax, semantics, lowering, and runtime behavior, read DEVELOPING.md. It is repository-only developer documentation and is not published on the website.
# Build all binaries with version info
make build
# Build specific binary
make build-sysml
make build-lsp
make build-grpc
# Build all three for both WebAssembly targets; see docs/reference/wasm.md
make build-wasm
# Install to $GOPATH/bin
make install
# Protobuf schema (api/proto/sysml.proto), all codegen driven by buf
make proto # regenerate the Go, Python, TypeScript and Rust stubs
make proto-buf # Go stubs only (the Java client generates its classes at Maven build time)
make python-proto # Python stubs only (needs grpcio-tools; PYTHON=... picks the interpreter)
make proto-lint # lint the schema, as CI does
make proto-breaking # reject wire-breaking changes against develop, as CI does
# Python gRPC bindings
make python-install # install opensysml package
make python-test # run Python binding tests
# Clean build artifacts
make clean# All tests (using Makefile)
make test
# All tests (direct)
go test ./...
# With coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# With race detector
go test -race ./...
# Short tests (faster, no race detector)
make test-short
# Specific package
go test ./internal/syntax/parser
# The WebAssembly gate: compiles and vets both wasm targets, links the commands
# and runs them under Node (docs/reference/wasm.md)
make wasm-checkParser-specific tests: When modifying the parser, ensure the four-layer test contract passes:
- Conformance gate:
go test -run TestStdlibConformance ./internal/workspace/libs - Golden ASTs:
go test -run TestGolden ./tests/parser - Negative tests:
go test -run TestNegative ./tests/parser ./internal/syntax/parser - Update goldens (after intentional changes):
go test -run TestGolden -update ./tests/parser
See docs/internals/architecture.md for full details on the parser testing contract.
Size caps on tests and fixtures: go test ./tests/hygiene fails when a _test.go file is over
2,000 lines or a fixture (a file under a testdata/ directory or named *.golden.*) is over
200 KiB. Split a test file along its feature seams (see AGENTS.md §5.2) and shrink the model a
fixture is generated from, or digest its output, rather than growing either further. The files
that predate the caps are allowlisted in tests/hygiene/size_test.go at their current size with a
one-line reason (lines for a test file, bytes for a fixture); an allowlisted file may not grow, and its entry is removed once it is back under
the cap. A new entry needs a reason a reviewer accepts.
make lint runs the same two checks CI gates on, at the versions pinned in the
Makefile:
- staticcheck — must report nothing. Unused code is deleted rather than left behind; if a helper is genuinely needed by work in flight, land it with its caller.
- gosec — must report nothing. Generated protobuf code is excluded
(
-exclude-generated). Suppress a finding with#nosec <rule>only with a comment saying why it is safe; do not widen the exclusion list.
- Use
gofmt(enforced by CI) - Follow Effective Go
- Write tests for new functionality
- Document exported types and functions
Follow conventional commits format:
<type>(<scope>): <description>
[optional body]
[optional footer]
Types:
feat: New featurefix: Bug fixdocs: Documentation changestest: Test additions/changesrefactor: Code refactoringchore: Build/tooling changes
Examples:
feat(parser): add support for state machine transitions
fix(runtime): handle null values in expression evaluation
docs: update Quick Start guide with new commands
test(semantics): add conformance checking test cases
The repository follows git-flow with two long-lived branches:
developis the integration branch and the default branch. Everyfeature/,fix/,docs/,ci/andtest/branch is cut fromdevelop, and its pull request targetsdevelop.mainis the release branch. It receives onlyrelease/x.y.zpull requests (cut fromdevelop, with the changelog fragments folded in) andhotfix/pull requests. Release tags —v*and the client package tags — are created onmain. After a release is tagged,mainis merged back intodevelop(a plain merge, no rebase) so hotfixes and the folded changelog flow down. See docs/project/releasing.md.
- Fork the repository
- Create a branch from
develop:git checkout -b feat/my-feature - Make changes with clear commit messages
- Run tests:
go test ./... - Push to your fork
- Open a Pull Request targeting
develop, ready for review (a draft is only for work you do not yet want reviewed; see CI tiers)
- Include tests for new features
- Update documentation as needed
- Ensure CI passes (build + tests)
- Keep PRs focused (one feature/fix per PR)
- Respond to review feedback
For maintainers:
- Update version (if needed in code)
- Merge the
release/x.y.zpull request intomain(see docs/project/releasing.md) - Tag the release on
main:git tag -a v0.1.0 -m "Release v0.1.0: Initial public release" git push origin v0.1.0 - CI automatically:
- Builds binaries for all platforms
- Publishes to GitHub Releases
- Merge
mainback intodevelop
- All tests pass
- Documentation updated
- Changelog fragments folded in (
python3 scripts/changelog.py release X.Y.Z) - Version tag follows semver (
vX.Y.Z) - Version bump (patch or minor) justified against § Versioning
- The first release carrying the analysis engines'
-jsonadditions — theplanandresults[]keys of each check and the standing line ending itslines— decides whether they are patch (new keys beside the existing ones, no existing key changed: the rule's "new wire fields are patch material") or minor (the rule's "the shape of a-jsonreport" changing so that a consumer validating the report against a closed schema fails), and records the decision here - Release notes prepared
Versions are 0.MINOR.PATCH until 1.0 and follow
Semantic Versioning, whose §4 leaves the meaning of the
segments open while the major version is zero. This project decides the segment by model
compatibility, not by the size of the release or the number of features it carries.
A release is a PATCH when everything the previous release accepted still behaves the same:
- Every model the previous release accepted is still accepted, with the same diagnostics.
- A model that ran gives the same results. The one exception is a bug fix that turns a wrong result into the one the Kernel Semantic Library derives; that is compatible and is listed under Fixed.
- No CLI flag, REPL command, RPC or wire field is removed or renamed. The protobuf
wire-compatibility check passes against the previous release's schema:
make proto-breaking BUF_BREAKING_REF=origin/mainon the release branch.
New features, new flags, new capabilities and new wire fields are all patch material.
A release is MINOR when any of that does not hold:
- A construct the previous release accepted is refused.
- A result the previous release derived correctly changes.
- A flag, command, RPC or wire field is removed or renamed.
- A fixture or output format changes so that existing user artifacts fail — an
.expected.jsonschema, the trace goldens, the shape of a-jsonreport.
MAJOR is reserved for 1.0; from 1.0 on, the conventional rule applies — MAJOR for breaking changes, MINOR for features, PATCH for fixes.
The changelog entry for a release says nothing about which segment was bumped or why; the decision is recorded through the Release Checklist item above.
.github/workflows/pr.yml is the only CI a pull request runs, in one of three tiers. Its
Build and test job aggregates whichever tier ran, so that is the one check branch protection
needs to require.
| Event | Tier | Race detector | Corpus the examples/ sweeps walk |
Derived-property differential |
|---|---|---|---|---|
| A push to a pull request | quick | none | whole | on what can move its baseline |
The merge queue's commit (merge_group) |
queue | every shard but the sweeps | the committed construct-cover subset | no |
The label ci:full, an unclaimed path, the nightly full scan, workflow_dispatch |
full | every shard | whole | on what can move its baseline (everything on an unclaimed path) |
The derived-property differential (Derived-property differential reproduces its baseline,
go run -C tools ./cmd/pilot-apijson-diff -check: Java, the pinned pilot jar, 18–25 minutes)
runs on a pull-request push whose diff can move the api-json baseline — the writer, the
referees, the pilot bridge and pin, the baseline, the corpora action, not the workflow file —
and under the full tier, so the baseline is also reproduced every night over develop. The
queue tier skips it: the merge group's diff is the one the pull request's push already ran it
over, and the nightly catches a baseline that develop moved under it, which is then
re-recorded through the ci:rebaseline workflow (.github/workflows/rebaseline.yml).
The examples/ sweeps are the tests that parse and export every model under examples/ (the
committed models, the OMG training and pilot corpora and the specification examples, some 920
files); scripts/race-shard.sh --sweeps prints the shards that hold them, and the queue tier
reads that list rather than naming it. Under OPENSYSML_CORPUS_SUBSET=construct-cover they walk
tests/corpus/testdata/construct_cover.txt, the regenerable subset of models that exhibits every
syntactic construct the whole corpus does; unset, they walk the whole corpus.
The quick tier is what every push to a pull request gets, draft or ready, in about half the
runner time of the full tier and a third of its wall time: the binaries, the same Go suite without
-race in ten groups of the race shards (make test-quick), the download-free static checks
(gofmt, vet, the production build, grpc imports, proto lint and breaking), the WebAssembly, Xpect
and conformance gates, and only the clients, editors and docs whose own paths changed. The lint,
the corpus and solver gates, the PDF toolchain and the fan-out from a service change to every
client wait for the full tier. Only a push touching a path no area claims escalates itself to
the full tier (scripts/ci-changed-areas.sh, and its test pins it).
The queue tier runs on the merge queue's commit (below). It runs every job and gate of the
full tier — gofmt, go vet, make lint, every shard of the test suite (twenty, make test-queue), the binaries, the client suites, the conformance suite over each transport, the
protobuf lint and wire-compatibility checks, the documentation hygiene checks (make docs-check,
make man-check and the census check), the site build, the PDF toolchain, and in each client's
job the release-digest copy it ships and the stubs it commits — with two economies, both on the
examples/ sweeps alone: those shards run without the race detector, and over the
construct-cover subset rather than the whole corpus (in the shards and in the static job's two
round-trip gate steps). Every other shard runs under -race, over everything, so what the queue
tier does not do is run the race detector over the sweeps, and walk the whole corpus. The
nightly full scan does both on develop's head.
The full tier runs on any pull request carrying the label ci:full (adding it re-runs the
current head), on an unclaimed-path push, on develop's head every night
(.github/workflows/full-scan.yml, 12:07 UTC, the hour the runner pool is idlest; it skips a
head that already has a full-tier run) and on demand (workflow_dispatch of pr.yml on any
ref, with a tier input that defaults to full). It is the queue tier without the economies:
the race-enabled suite over the whole corpus (make test-shard), and it downloads the OMG
corpora before the suite and runs each corpus gate as its own step, so those gates are required
rather than skipped. CircleCI's go-race-test, go-coverage and the release workflow are the
full scan too: a push to main, and a v* tag whose commit has no full-tier run, run
everything there, over the whole corpus, with -race.
The corpus subset. The tests that sweep every model under examples/ (the committed models
and the downloaded OMG corpora) honour OPENSYSML_CORPUS_SUBSET: unset, they iterate the whole
corpus; OPENSYSML_CORPUS_SUBSET=construct-cover makes each iterate only the files of
tests/corpus/testdata/construct_cover.txt, which between them exhibit every syntactic feature
the whole corpus exhibits, and any other value fails the sweep. TestConstructCover
(tests/corpus) re-derives the features over the whole corpus on every run and fails, naming
them, when the committed list no longer covers it; re-record with
go test ./tests/corpus -run TestConstructCover -update-construct-cover. No recorder flag runs
while the variable is set, so an expectation file is never recorded from a partial sweep.
docs/project/construct-cover-subset.md records the
measured subset size and feature count and says what the switch does to each sweep.
Open a pull request ready for review rather than as a draft: both run the same tier, and a draft
is not considered until it is marked ready. The queue tier is what a pull request has to pass to
merge, and ci:full runs the full tier early when a change warrants it (a new goroutine, a
shared cache, anything the race detector should see over the whole corpus before the nightly
does).
The merge queue, when the develop ruleset has one, runs the queue tier on the commit it
builds for each queued pull request (refs/heads/gh-readonly-queue/develop/..., the
merge_group event): the diff is the merge group's own, base_sha..head_sha, the run is never
a draft, and Build and test reporting on that commit is what lets the entry merge. A queue
commit therefore gets at least the checks a ready pull request with the same diff gets. The Go
test jobs are named by tier — Go tests (…), Go queue tests (…), Go race tests (…) — and
scripts/ci-verified-tree.py reads the names back: CircleCI's build-test on develop skips
the suite over a commit with a queue-tier or full-tier run, while build-test on main and a
tag's release workflow accept only a full-tier run (a tag also a green build-test of main
on the same commit); see releasing.
The first job, Changed areas, runs scripts/ci-changed-areas.sh, which decides the tier from
the event, the label ci:full and the paths (CI_TIER=quick|queue|full forces it), and the
areas from the pull request's files; the rest of the jobs are gated on what it reports: a
change confined to one client runs that client's job alone, while a change to the Go sources,
the proto, conformance/ or the workflows runs everything (under the queue and full tiers;
CI_TIER=quick keeps the clients and docs to their own paths). A path no area claims turns
every area on, so a new directory is over-tested rather than untested — teach the script about
it, and add a case to scripts/ci-changed-areas-test.sh, which the same job runs.
Every third-party action a workflow or composite action uses: is pinned to a full commit SHA
with the release that commit is as a trailing comment (actions/checkout@<sha> # v4.4.0); a new
step takes the same form, and the comment is what lets the pin be moved by version.
Dependency audits run in both tiers, each in the job whose ecosystem it covers and gated
by the same Changed areas output as that job: make vulncheck (govulncheck over both Go
modules, failing on a vulnerability the code reaches) in the Go static job, make audit-npm
(npm audit --omit=dev --audit-level=moderate over every package-lock.json) in each Node job,
make audit-python (pip-audit --strict over client/python, client/jupyter-kernel,
packaging/pypi-pysysml and docs-requirements.txt) in the Python job, make audit-rust
(cargo audit) in the Rust job and make audit-java (a CycloneDX SBOM of each Maven tree —
client/java, editors/syson, editors/mdk — scanned by osv-scanner, test scope excluded) in
the Java, SysON and MDK jobs. The tools come pinned
and SHA-256 verified from scripts/download-sca-tools.sh (make sca-tools); make audit runs
all five locally. A finding fails the job: fix it by bumping the dependency (a Dependabot pull
request may already exist), never by lowering a level or ignoring an advisory. The same audits
run every night over develop (nightly snapshots), so an advisory
published against an unchanged tree is seen within a day and no snapshot is published over a
failed audit, and before a release
(releasing). The npm level is moderate, the
lowest npm audit ranks a real advisory at; low is not used because npm reports it for
advisories without a fix.
.github/dependabot.yml opens at most one grouped pull request a week
per ecosystem (both Go modules, every npm lockfile, the Python packages, the Rust client, the
three Maven trees, and the GitHub Actions the workflows pin), against develop, with minor and
patch updates in one group and a Conventional Commits title (chore(deps): …). Major versions
are not offered at all (open-pull-requests-limit: 1 and a version-update:semver-major ignore
in every entry): the workflows pin actions to commit SHAs and the TypeScript and Node majors
break the builds, so a maintainer takes each major by hand. Dependabot alerts and security
updates are repository settings that this file does not affect. Such a pull request is
handled like any other: it opens ready for review, so the full tier runs on it with Changed areas scoping the jobs to the lockfile it touched, Devin Review and the dependency audits run on
it, and a maintainer merges it once green; rebase it with a @dependabot rebase comment, never
by hand. A security update Dependabot opens outside the weekly schedule follows the same path.
The one dependency it leaves alone is editors/vscode's @types/vscode, pinned at the
extension's engines.vscode floor because vsce package refuses a newer typing: raise the two
together, and tests/hygiene fails when the typing exceeds the floor or the ignore rule is gone.
The npm entry updates with versioning-strategy: increase-if-necessary, so a package.json
requirement is rewritten only when the new version falls outside it: the exact pins of
client/node, editors/vscode, editors/syson/frontend and scripts/asciidoc still move with
every update, while editors/jupyterlab's ^4.0.0 ranges on @jupyterlab/application and
@jupyterlab/codemirror — the JupyterLab 4 hosts the extension supports, not what it is built
against; the lockfile and the jupyterlab== pins of the workflows decide that — stay as written.
Dependabot alerts and security updates are repository settings a maintainer switches on; the
configuration file only schedules version updates.
.circleci/config.yml runs after a merge and on tags, never on a pull request branch:
On every push to main or develop:
- The same suite, gates, client tests and conformance runs as the pull-request workflow, over the merged tree, plus the host binaries
- SonarCloud scan, fed by the Go and client coverage reports
The conformance job stores its JSON report as an artifact and its JUnit XML
(bin/conformance-report.xml) as test results, so a failing scenario is named in the Tests tab
rather than only in the log. The README badge tracks this workflow.
On tags (v*, and the client package tags):
- Run the suite again on the tagged revision
- Build release binaries (all platforms)
- Create GitHub Release and upload binaries
- Publish the client packages
PRs must pass the GitHub Actions Build and test check under the queue tier (the merge queue's
run; the full tier adds the race detector over the examples/ sweeps and the whole corpus),
which requires:
- Build succeeds
- All tests pass
- No race conditions
- Code formatted (
gofmt) - Static analysis clean (
make lint: staticcheck and gosec) - Dependency audits clean (
make audit: govulncheck, npm audit, pip-audit, cargo audit, osv-scanner) - OMG corpus gates run (not skipped) and match their expectations
- Protobuf schema lints and is wire-compatible with the base branch
- Documentation checks pass (
make docs-check,make man-check) and the site builds
github.com/Open-MBEE/OpenSysML
├── cmd/ # Binaries (sysml, sysml-lsp, sysml-grpc)
├── internal/ # One directory per layer; a package imports only the layers below it
│ ├── syntax/ # source, diag, lexer, parser, ast, pack, format
│ ├── semantic/ # symbols, resolve, suggest, semantics, identity, highlight, query
│ ├── ir/ # lower, queryplan, docplan, view
│ ├── check/ # passes, edit
│ ├── exec/ # runtime, solve, smt, analysis, engines, objref
│ ├── translate/ # rdf, export, xmi, migrate, convert, codegen, interop
│ ├── doc/ # queryexec, docir, docrender, docpdf
│ ├── workspace/ # model, libs, project, envvar
│ └── frontend/ # protoconv, grpc, lsp, repl, stdiorpc, usage
├── client/python/ # Python client bindings (opensysml)
├── client/rust/ # Rust client (opensysml) and its conformance runner
├── docs/ # Documentation
│ ├── guide/ # The handbook, in reading order
│ ├── reference/ # CLI, REPL, environment, APIs, RDF mapping
│ ├── internals/ # Architecture, testing, performance, design notes
│ └── project/ # Compliance, roadmap, releasing, measurements
├── tests/ # Black-box suites, benchmarks, shared fixtures (tests/parser, tests/testdata, …)
└── .circleci/ # CI configuration
Documentation is organized by what a reader wants, not by the feature that landed. Four areas, mapped in docs/README.md and published as https://runtime.opensysml.org/:
- docs/guide/ — how do I use it? A numbered handbook read in order.
- docs/reference/ — what does this flag, command, API or triple mean? Look-up material, exhaustive rather than narrative.
- docs/internals/ — how is it built? For someone changing the code.
- docs/project/ — where does the project stand? Compliance, roadmap, release and testing process, and measured counts.
When a change needs documenting:
- Extend the chapter that already covers the surface rather than adding a page per feature —
a new REPL command belongs in reference/repl-commands.md and
the chapter that teaches the workflow, not in a new
MY_FEATURE.md. - Explain in the guide, enumerate in the reference. Do not repeat a flag table in both; link to it.
- Measured numbers have one home (fixture, corpus and conversion counts live in docs/project/); elsewhere, link to it instead of restating a number that will drift. Two release-gate surfaces are the deliberate exception, because docs/project/releasing.md checks the numbers they print: the gate tables in docs/project/roadmap.md and docs/project/training-examples.md. Recount both together, in one commit.
- The test-suite figures are counted at build time, never committed. The conformance-case,
golden-AST, golden-trace, negative-parser, robustness, gRPC and
Test-function counts in the compliance map's test inventory are<!-- doc-counts:begin inventory-… -->blocks whose committed text names what is counted and states no figure; the site build (scripts/mkdocs_suite_figures.py, run bymake docs) splices in the figures fromgo run -C tools ./cmd/doc-counts -site-blocks, which counts the tree the way the gates enumerate it.go run -C tools ./cmd/doc-counts -checkrefuses a figure typed into one of those blocks, so adding a test or a fixture is the whole change and two branches cannot conflict on a count. The api-json differential record (docs/project/api-json-differential.md) carries its scorecard headline, per-root table and the figure columns of its gap tables the same way, asapijson-…blocks the hook fills fromgo run -C tools ./cmd/pilot-apijson-diff -site-blocks(the committed baseline); the committed rows hold only the (metaclass, property) key and the adjudication, so re-recording the baseline never rewrites the page, andTestRecordStatesTheBaselinesFigures(tools/referee/apijson) refuses a figure typed into a block.README.mdnames the gates without their counts; the one suite figure still committed there is whether every conformance case passes, which moves withknown_failures.txtalone. - Robustness cases are registered per feature. A runtime failure-mode subtest goes in
internal/exec/runtime/robustness_<feature>_test.gounder aTestRuntimeRobustness<Feature>function (gRPC:internal/frontend/grpc/robustness_<feature>_test.go,TestGRPCRobustness<Feature>), a new file for a new feature;robustness_test.goholds only the shared helpers and is not where new ones go.go testdiscovers them like any test, and the build-time counters sum everyTestRuntimeRobustness*andTestGRPCRobustness*function, so two branches adding cases never edit one registry. - The compliance-row census is counted at build time, never committed. Adding or changing a
✅/⚠️/❌/⛔row in the area's source under docs/project/spec-compliance/ is the whole change: no header,README.mdline ordocs/internals/architecture.mdline restates the count, so two branches that both add rows cannot conflict on one. The site build (scripts/mkdocs_census.py, run bymake docs) counts the rows into the<!-- doc-counts:begin census -->block and refuses a🚧row, as domake docs-countsandgo test -C tools ./referee/diff.make docs-countsstill restates the externally refereed oracle numbers from the baseline JSONs (and the README's conformance-passing sentence); run it only when a baseline orknown_failures.txtmoved. - A baseline conflict with
developis re-recorded by a label, not by hand. When a branch anddevelopboth re-recorded a generated baseline (the api-json differential, the derived census, the structure differential, the library/prefix sweep, the corpus and round-trip expectations, the conversion and parser goldens, the proto stubs), apply theci:rebaselinelabel to the pull request. .github/workflows/rebaseline.yml mergesorigin/developinto the branch takingdevelop's side of the conflicts confined to the generated paths it lists (a conflict anywhere else fails the run), re-runs every recorder on the merged tree, refuses a movement the merge does not explain (scripts/rebaseline-movement.py: a verdict that moved for a record neither side moved), commits the result with the movement in its message, pushes to the branch, removes the label and comments the movement for adjudication. It never touchestests/corpus/testdata/training_examples_expected.txt, which is asserted clean rather than ratcheted, and it needs the label applied to the branch's current head (a push after the label stops it). Only a branch of this repository can be re-recorded, not a fork's. - Changelog entries are fragments, not edits to
CHANGELOG.md. A change that a user should read about adds one file,changes/unreleased/<slug>.<section>.md, holding the list item(s) for that section (added,changed,fixed, …); see the README there. Two branches then never touch the same changelog lines.python3 scripts/changelog.py checkvalidates the fragments and runs in CI; the release procedure folds them intoCHANGELOG.md. - Internal work-item labels stay out of what a reader reads. Waves and slices (
wave 12A,W8G), follow-up rows (F4), adjudication probes (P1) and diagnostic classes (K5,S10) have no public referent, soCHANGELOG.md,README.md, the guide, the reference, the internals pages — and PR bodies and release notes — say what the change did instead.python3 scripts/check-doc-ids.pyfails on one and runs in CI. Thedocs/project/conformance records are the exception: they cross-reference each other by these labels and each defines them in a note at the top. A real keyboard shortcut is not an internal label; spell it<kbd>F2</kbd>. - An oracle total quoted outside the generated block is a snapshot, and says so. The totals of
the differential, Xpect and rejection oracles move with every rule, fixture and pin change, so
only the
doc-countsblock states the current ones. A page that quotes a figure from the round it documents must say it is not the current baseline;python3 scripts/check-doc-figures.pyfails on one that does not, and runs in CI. - A new page goes in the
nav:of mkdocs.yml, in the reading order of its area, or it is published but unreachable from the site's navigation.make docs-installonce, thenmake docsbuilds the site the way CI does andmake docs-servepreviews it. - Moving or renaming a page means updating its inbound links. Run
python3 scripts/check-doc-links.py— it fails on a link to a missing file or heading and runs in CI. Where a releasedREADME.mdlinked the old path, leave a one-paragraph pointer behind there (docs/ARCHITECTURE.mdand friends are such pointers); a page only ever linked from insidedocs/is moved outright.
See ARCHITECTURE.md for detailed design.
Key principles:
- Immutable AST: Syntax-only, never mutated
- Side tables: Semantic info keyed by node/symbol
- Lazy evaluation: Compute on-demand, memoize
- Incremental: Invalidate only affected parts
- Unit tests: Per-package (
*_test.go) - Integration tests: Cross-package scenarios
- Fixtures: Real SysML v2 models in
tests/testdata/ - Golden files: Expected outputs (where applicable)
- GitHub Issues: three forms, picked when you open one — a tool bug (OpenSysML does something other than what it documents), a spec conformance gap (it disagrees with SysML v2 or KerML, so name the clause), or an objection to a ruling (the behavior is deliberate and the report argues that the interpretation is wrong). Check spec compliance and omg-issues.md first: a known divergence is usually already a row there with the reasoning behind it.
- Discussions: questions, ideas, feature requests
- Pull Requests: code contributions. The template asks for the specification basis of a behavior change and how it was verified.
- Be respectful and inclusive
- Focus on constructive feedback
- Help others learn and grow
- Follow project conventions
By contributing, you agree that your contributions will be licensed under the same license as the project (see LICENSE).
Thank you for contributing to OpenSysML.