Prime directive: If I wanted hacks, I'd write it myself. Don't ever choose hacky over correct. Fix root causes upstream, not symptoms. Never weaken, skip, or delete tests to make them pass.
OpenSysML is a production-grade SysML v2 implementation in Go 1.23+ (module github.com/Open-MBEE/OpenSysML).
It provides a hand-written lexer/parser, semantic engine, execution runtime, LSP server (sysml-lsp), and REPL (sysml).
- Correctness over expedience. No shortcuts, no stubs left behind, no lossy conversions. If a proper fix is large, do it properly or stop and flag it.
- For features specifically: do not minimize code changes or dodge complexity. Implement the feature fully and correctly even if it touches many files, adds new types, or requires refactoring. Completeness beats diff size. See §8.
- Root-cause first. Before editing, confirm why something fails (read the code, add a temporary debug print, write a focused test). Then make the minimal correct change.
- Never regress.
developis green. Any test passing ondevelopmust still pass on your branch. Diff againstdevelopif unsure:git stash && git checkout develop && go test ./... ; git checkout - && git stash pop. - Respect the architecture invariants (see §4). The AST is immutable; semantics live in side tables; execution consumes lowered IR — do not bypass these.
- Tests are the contract. Existing tests encode intended behavior (including when and where errors surface). Make code satisfy tests, not the reverse — unless the test is provably wrong, in which case explain before changing it.
- Leave no dead code. Remove superseded helpers/structs. Run
go vet ./...to catch it.
Use the Makefile (preferred) or raw go commands. Never cd — run from repo root.
make build # build bin/sysml and bin/sysml-lsp (with version ldflags)
make build-sysml # REPL binary only
make build-lsp # LSP binary only
make test # full suite: go test -race -coverprofile ... ./...
make test-short # faster, no race detector
make clean # remove build artifactsRaw equivalents / targeted runs:
go build ./... # must always be clean
go vet ./... # must be clean (catches unused/dead code)
go test ./... # all tests
go test ./internal/exec/runtime/... # one package tree
go test -run TestExecutionConformance ./internal/exec/runtime
go test -race ./... # race detector (CI runs this)
gofmt -l . # must print nothing (CI enforces gofmt)Definition of done for any change: go build ./..., go vet ./..., gofmt -l . (empty), and go test ./... all pass. Paste the results.
The OMG training-corpus gate is part of that suite but skips while the corpus is absent, so
fetch it once with ./scripts/download-training-examples.sh and re-run
go test -count=1 ./tests/corpus -run TestTrainingExamples. CI downloads the corpus
too and sets OPENSYSML_REQUIRE_TRAINING_CORPUS=1, so there an absent corpus fails rather
than skips.
The three OMG pilot corpora are gated the same way: fetch them with
./scripts/download-pilot-corpora.sh and run
go test -count=1 ./tests/corpus -run TestPilotCorpora. CI sets
OPENSYSML_REQUIRE_PILOT_CORPORA=1. See docs/project/pilot-corpora.md.
So is the pilot's XMI of the standard library, which the identity gate reads: fetch it with
./scripts/download-pilot-library-xmi.sh and run
go test -count=1 ./tests/identity -run TestPilotLibraryXMI. CI sets
OPENSYSML_REQUIRE_PILOT_LIBRARY_XMI=1. Whatever sets a require variable must run the matching
download script first; the scripts are idempotent, and none reports success over an empty corpus.
All four roots share one mechanism (tests/corpus/corpus_gate_test.go) but two
policies, and the difference is deliberate: the training corpus is asserted clean, so its
expectation file holds no per-file counts and -update-training refuses to record one, while
the other three are a per-file ratchet whose every movement must be adjudicated. Do not
turn the assertion into a ratchet.
The RDF mapping has a per-file ratchet of its own over every model under examples/, the
downloaded corpora included: TestCorpusRoundTrip in tests/corpus converts each file
notation → Turtle → notation → Turtle and pins the verdict. Run it with both require variables
set after any change to internal/translate/export, adjudicate every movement, then regenerate with
-update-corpus-roundtrip. See docs/project/rdf-corpus-roundtrip.md.
cmd/
sysml/ REPL binary
sysml-lsp/ LSP server binary
internal/ one directory per layer; a package imports only the layers below it
syntax/ source files and spans, diagnostics, lexer, parser, AST, pack, format
semantic/ symbol tables, name resolution, suggestions, semantics, identity, highlight, query
ir/ lowered execution IR (ActionGraph, StateGraph), query/doc plans, views
check/ tiered validation passes (syntax → nameres → type → constraint), workspace edits
exec/ execution engine, SMT solving, analysis framework and engines
translate/ RDF, XMI and notation conversion, code generation, interop
doc/ query execution, document IR, Markdown/HTML/PDF backends
workspace/ workspace and document management, stdlib bundling, project files, env vars
frontend/ LSP, REPL, gRPC and stdio transports, protobuf conversion, usage
tests/ black-box suites and their fixtures
hygiene/ module-wide checks (no production code imports testing)
perf/ benchmark harness (go test ./tests/perf -run '^$' -bench .)
testutil/ gobuild (build a command under test), graphcmp (pointer-graph comparison)
parser/ golden ASTs (TestGolden, -update) and negative cases, with testdata/parse
grpc/ gRPC conformance cases (TestGRPCConformance) driven over the RPC surface
export/, resolve/, … external-package (package x_test) suites, each beside its own testdata
testdata/ shared fixtures (.sysml, .kerml, .golden)
examples/ example models and demos
docs/ guide/ (handbook), reference/, internals/, project/ (status)
Read docs/internals/architecture.md before non-trivial work — it documents the pipeline, tiers, and test contracts in depth.
- Immutable AST.
internal/syntax/astis syntax-only and is never mutated after parsing. All derived/semantic data lives in side tables keyed by node/symbol. - Parser never fails.
parser.New(src).ParseFile()always returns a tree; malformed input yieldsErrorNodes + diagnostics, never a panic. - Lazy + memoized semantics. Name resolution and type queries compute on demand and cache. Don't force eager work.
- Tiered passes. Higher validation tiers are skipped when a lower tier errors, unless a pass declares
passes.ElementScopedand gates itself per subject viaContext.DownstreamOfFailure. Keep passes independent and level-scoped. - Runtime consumes lowered IR. Executors should operate on
internal/ir/lowergraphs (ActionGraph/StateGraph) as the single source of truth — do not re-parsesymbol.Declinside executors, and do not build parallel/duplicate structures that can drift. Lowering must be lossless (carry guards, triggers, effects, pseudostate edges). - Error timing is part of the contract. Constructors (
newActionExecutor,newStateExecutor) succeed on structurally-empty inputs; "no initial node/state" errors surface atinitialize(). Don't move error points without updating the corresponding tests intentionally.
When touching the lexer/parser or adding grammar:
- Conformance gate:
go test -run TestStdlibConformance ./internal/workspace/libs— all official stdlib files must still parse clean (no regressions). - Golden ASTs:
go test -run TestGolden ./tests/parser. Add a representative fixture undertests/parser/testdata/parse/*.sysml. - Negative tests:
go test -run TestNegative ./tests/parser ./internal/syntax/parser— malformed input must produce diagnostics without panicking. - Update goldens only after intentional changes:
go test -run TestGolden -update ./tests/parser, then review the diff carefully.
- Golden AST fixture locking parse structure (
tests/parser/testdata/parse/). - Execution conformance: add
.sysml+.expected.jsonunderinternal/exec/runtime/testdata/conformance/; rungo test -run TestExecutionConformance ./internal/exec/runtime. Schema is documented in that dir'sREADME.md. - Golden execution traces for ordering-sensitive behavior (fork/join, transitions):
go test -run TestExecutionTrace ./internal/exec/runtime(update flag:-update-traces). - Robustness: add a failure-mode case (deadlock, unbound params, missing refs, dangling transitions, step budget) as a subtest of a
TestRuntimeRobustness<Feature>function ininternal/exec/runtime/robustness_<feature>_test.go— a new file for a new feature, so branches never edit one shared registry;robustness_test.goholds the shared cases and is not where new ones go. Must return typed errors, never panic or hang. The suite counters read everyTestRuntimeRobustness*function, and gRPC cases follow the same pattern withTestGRPCRobustness*.
Then update docs/project/spec-compliance.md mapping: semantic rule → implementation (file:function) → test → status (✅ faithful /
- Unit tests live beside code as
*_test.go, one concern per test. - Design/adjust tests before or alongside implementation; don't retrofit weak tests afterward.
- Prefer real SysML models in
tests/testdata/over hand-built ASTs when exercising end-to-end behavior; hand-built ASTs are fine for targeted unit tests.
- Branch from
develop, targetdevelop.developis the default and integration branch;mainis release-only (release/x.y.zandhotfix/PRs, cut by a maintainer). Cut ordinary development branches fromorigin/developand open their pull requests againstdevelop— nevermain. Onlyrelease/x.y.z(cut fromdevelop) andhotfix/(cut frommain) branches targetmain; seedocs/project/releasing.md. Branch names are git-flow style,<type>/<short-slug>(see CONTRIBUTING.md § Branches). - Understand first. Grep/read the relevant package and its tests. Diff the branch against
developto see what changed and why. - Reproduce. Run the failing test(s) and read the exact error before changing anything.
- Locate the root cause in the correct layer (lexer vs parser vs lower vs runtime). Bugs in specialized layers are often upstream of where they surface.
- Implement the correct fix. For bug fixes, keep edits minimal and scoped. For features, implement completely (see §8) — "minimal" means no unrelated changes, never under-built. Match existing style; keep imports at the top.
- Add/adjust tests to lock in the fix and cover the failure mode.
- Verify with the full gate in §2. Remove any temporary debug code and dead code.
- Commit using Conventional Commits (see §7). Keep PRs focused (one feature/fix each).
- Formatting:
gofmtis mandatory (CI-enforced). Follow Effective Go. Document exported types/functions. - Changelog: add a fragment, never edit
CHANGELOG.md. Write the entry tochanges/unreleased/<slug>.<section>.md(<section>isadded,changed,fixed, …; body is the list item(s) only — see the README there). Concurrent PRs then cannot conflict on the changelog; the release procedure folds fragments in.python3 scripts/changelog.py checkvalidates them. - Comments: don't add or remove comments/docs unrelated to your change.
- Commit messages — Conventional Commits:
<type>(<scope>): <description>- types:
feat,fix,docs,test,refactor,chore - e.g.
fix(runtime): preserve transition effects when lowering state graph
- types:
- Never write internal work-item labels into anything a user reads. Waves and slices
(
wave 12A,W8G), follow-up rows (F4,F84–F95), adjudication probes (P1) and diagnostic classes (K5,S10) are this project's private bookkeeping — a reader has nothing to resolve them against. Write what the change did instead: not "F3 unreserved these four", but "these four are unreserved by file kind".- This applies to
CHANGELOG.mdandchanges/unreleased/,README.md,docs/guide/,docs/reference/,docs/internals/, doc comments, diagnostic messages, and equally to PR titles/bodies, release notes and GitHub comments — CI only guards the files (python3 scripts/check-doc-ids.py, ormake docs-checkwith the link checker), so the prose you write around them is on you. - The conformance records under
docs/project/are the one exception: they cross-reference each other by these labels, so each opens with a Labels note defining them. If you add a record that uses them, add that note too. - Real keyboard shortcuts (
F2to rename,F5in VS Code) are not internal labels — spell them<kbd>F2</kbd>so their meaning is unambiguous. - Identifiers in code (an
errata.Entry.ID, a test name) may keep their labels; when code points at documentation, point at the section's title, not at the label (errata.Entry.Heading).
- This applies to
Bug-fix discipline (small, scoped diffs) does not apply to feature work. For features, the goal is a complete, correct, production-grade implementation — not the smallest possible change.
Do not:
- Minimize the diff at the expense of correctness or completeness.
- Avoid touching many files, adding new types, or refactoring when the feature genuinely needs it.
- Stub, fake, hardcode, or special-case a path to make a demo/test pass while leaving the general case unhandled.
- Bridge/adapter around the real design (e.g. copying IR back into legacy fields) to avoid a proper migration — this drifts and loses data.
- Silently narrow scope. If you implement only part of a feature, that is a known limitation that must be called out, not hidden.
Do:
- Implement the whole feature, including the hard cases (nesting, hierarchy, orthogonal regions, error/edge paths), not just the happy path.
- Follow the layering. Put logic in the correct layer (lexer → parser → lower → runtime). If a feature needs new IR, extend
internal/ir/lowerlosslessly rather than re-deriving data downstream. - Refactor when the design requires it. If the clean implementation needs a new type, an interface change, or migrating existing callers, do that — and migrate all callers, deleting the superseded code.
- Prefer completeness over diff size every time the two conflict.
Workflow for a complex/multi-part feature:
- Plan and decompose. Break the feature into ordered, independently-verifiable steps. State the plan before large edits. For long-horizon work, keep a short scratch note (e.g.
progress.txt) of steps done / remaining — but don't leave it in the final PR.- When writing plans or specs: Write the outline first (structure, task headers, DoD), then fill in each section completely, one at a time. Never write placeholder TODOs or "fill in later" — complete each section before moving to the next.
- Design the data first. Decide the AST/IR/side-table shape before wiring behavior. Getting the representation right avoids downstream shortcuts.
- Write the test contract up front (§5). Add the conformance/golden/robustness cases that define "done" before or alongside the code, including the hard cases — so you can't accidentally under-build.
- Implement layer by layer, keeping
go build ./...andgo vet ./...green at each step. - Handle all cases explicitly. Every unsupported path returns a typed error with a clear message — never a silent no-op, panic, or wrong result.
- Remove interim scaffolding (bridges, temporary fields, debug prints, dead code) before finishing.
- Verify the full gate (§2) and update
docs/project/spec-compliance.mdwith honest status flags.
When a feature is genuinely too large to finish correctly in one pass: stop and flag it. Deliver a correct, complete subset with the remaining scope explicitly documented as known limitations + failing/t.Skip-with-reason tests or a known_failures entry. Never fake completeness.
- Bridge/adapter shims that copy IR back into legacy fields — these drift and lose data (e.g. dropping a transition's
Effect). Migrate consumers to the IR instead. - Re-parsing
symbol.Declinside executors — bypasses the lowering layer; use the graph. - Hard-failing in
ToActionGraph/ToStateGraphon missing initial — breaks the constructor-succeeds/initialize()-errors contract. - Forgetting
-update/-update-tracesafter an intentional golden change, or running them blindly and masking a real regression — always review the golden diff. - Leaving unused structs/functions after a refactor —
go vet ./...and clean them up. - Assuming where states/members live (Members vs Substates vs Regions) — verify against actual parser output.
See CONTRIBUTING.md and docs/internals/architecture.md for the authoritative, detailed references.