Baseline: v0.8.0 (238aed650, Merge pull request #277 from Open-MBEE/release/0.8.0,
2026-09-13), verified locally with Go 1.25.0. The newest tag on Open-MBEE/OpenSysML is the patch
v0.8.1 (e0ea34616, Merge pull request #347 from Open-MBEE/release/0.8.1, 2026-09-16), cut
from v0.8.0 by cherry-pick and carrying bug fixes alone, so it moves no item and v0.8.0 stays
the baseline the statuses are read against; before them v0.7.0 (e0fbfea5b, 2026-09-09) and
v0.6.0 (30f103bb9, 2026-09-07). The integration branch develop (857dd48b0, #517) carries
216 pull requests past v0.8.0 (#364 folded v0.8.1 back into it) and nothing else is counted
ahead of it: every status below is what the tag carries unless the text names one of those as
having moved it — #267, #289 and #293 (Q2), #359 (Q1's page and Q3), #263 (A7), #286 (the release
fold-back), #291 (the generated test figures), #292 (L7), #296 (A4), #335 and #352 (E1), #466 and
#479 (a braced state block as one action), #362 (E2), #465 (E4), #468 (dynamic objects), #344
(modeled randomness beside A3), #356 (W3's rasterization and export), #358 (the PDF path onto the
HTML backend, under "Proposed"), #300 and #316 (the large-model design and its first step, under
"Proposed"), #319, #321, #334 and #413 (the fUML referee for actions and its emitter, under
"Proposed"), #350 (R4's installer wizard), #304 (the errata overlay over the bundled library, under
"Upstream follow-through"), and the state-executor fixes and findings the PSSM referee adjudicated
(#295, #297, #311, #313–#315, #317, #318, #322, #326, #336, #342, #384, #438, #486, #490, #513;
Track E) — and
the pull requests open against develop at this baseline are named where they touch a roadmap
item.
Read AGENTS.md first; it governs everything below.
Labels. This is an engineering record. The RDF items keep the
Dnumbers (D1,D2,D3.4,D7,D8,D9,D10,D11,D12,D13) that other records, the known-violations inventory and the ontology package's README cross-reference;Lnames the library items,Nthe native compilation track,Rthe release follow-through,Wthe diagram output formats a view rendering is written in,Fthe executor defects the conformance gate carried as known failures (closed),Sthe multiple-valid-executions work the executor needed before Track E (landed),Ethe behavior-execution semantics of the runtime; every item is now landed or closed by design record, and this section records how each was decided,Xthe expression forms it parses but does not evaluate,Qthe runtime query surface,Aanalysis and simulation execution,Vthe validation census,Ithe language integrations,Bthe bindings from modeled elements to external data and services,Mthe embedded target, andPthe package layering of the Go module. Each is stated in full where it is introduced, and a reader who wants only the gap can ignore the label.Status words. Landed means in the tag named above, so a landed item is released; where a pull request merged to
developafter the tag moved an item, the text names it and the item is landed but unreleased. Open names a pull request againstdevelopthat exists and is not merged; conflicts means it no longer merges cleanly ontodevelopand needs a rebase before review. In progress means the work is being implemented and has no pull request yet. Not started means exactly that. A status is taken from the pull request itself, never from a branch name or a commit message.
v0.8.0 (238aed650, 2026-09-13) follows v0.7.0 (e0fbfea5b, 2026-09-09) and v0.6.0
(30f103bb9, 2026-09-07); v0.8.1 (e0ea34616, 2026-09-16) is the patch cut from it, whose
CHANGELOG.md 0.8.1 section lists the fixes it carries and nothing else. v0.8.0's
CircleCI release workflow succeeded: the release carries sysml, sysml-lsp and sysml-grpc for five platforms,
the Homebrew bundles, the cosign-signed manifest and — for the second release running — the
Windows installer opensysml-0.8.0-windows-amd64.msi (R4). The Python client's opensysml-v0.5.0
tag points at the same commit and PyPI serves opensysml 0.5.0 from it — its release-python
workflow failed twice in Go coverage profile and passed when rerun, so the post-tag checks in
docs/project/releasing.md are all met and nothing of the release step is left.
CHANGELOG.md's 0.8.0 section is the tag's content (#268 and #277 folded it), its 0.7.0
section is v0.7.0's (#145, #154, #159), and changes/unreleased/ on develop holds the
fragments of the pull requests merged after the tags alone, #286 and #364 having carried the
fold-backs of v0.8.0 and v0.8.1.
Between releases .github/workflows/nightly.yml publishes the newest green develop as the
prerelease nightly (#284), so an item landed after the tag is unreleased but not unbuilt.
Since v0.7.0 the repository works git-flow: develop is the integration branch, main
receives only release/x.y.z and hotfix/ pull requests (#151), and the roadmap's baseline is
therefore the tag rather than main's head.
Everything in "Release follow-through" is maintainer- or account-gated; everything after it is
ordinary engineering work.
What closed between v0.6.0 and v0.7.0 — 50 merged pull requests (#110–#161, less #143
and #160, which were closed unmerged), in CHANGELOG.md's 0.7.0 section for the detail, listed
here because each retires or narrows a roadmap line. The tracks below carry the detail and what each pull
request deliberately leaves.
- Executor. A node several successions reach performs once, after one token per succession,
and a join one of whose successions can deliver nothing deadlocks instead of firing on a count
(#116, closes F1 and F2); a merge is one performance per arrival, so a loop through a merge runs
to its exit (#120, closes F3); a breakpoint on a synchronized node pauses once and a step moves
each token at most once (#119); a via-less
accepton a transition no longer takes a transfer addressed to a port (#126); a paused%action/%staterun resumes with its own budgets, choice points and memo rather than another run's (#130); a long race-instrumented run no longer grows step by step (#146); a sourceless transition leaves the state declared before it, the guarded entry transition chooses by its guards, and a machine's ownentry/do/exitrun for every machine (#155, #161).known_failures.txtis empty. - Multiple valid executions. A conformance case lists the outcomes the library admits and a
.trace.orderfile states a partial order on its trace (#110, closes S1); every choice the library leaves open is reported as achoicetrace line, a diagnostic and a REPL summary (#123, closes S2); the scheduling policy is selectable —reversethe default,declared,seed:<n>— on the CLI, in the REPL, on the wire and as a conformance-case pin (#125, closes S3);exploreruns every linearization one token per step and tables the distinct outcomes, and the harness explores every case that listsoutcomes(#134, #153, closes S4); the order two regions react to one event in is a choice point under every policy andseed:<n>varies it (#141); the oracle, the compliance record, the behavior guide and a design note state which orders are open and how each is checked (#138, #150). - Expressions and values.
x as Tevaluates as classification, including composed types (#115, closes X3);*is a value andelem.metadatareads an element's metadata (#113, closes X4); a calc is a value — passed, returned, read off a part, applied through acalc-typed parameter — and crosses the wire asValue.function(#122, closes X6);Collections::Setis held as a set and tensor quantities take any rank, both crossing gRPC whole (#121, closes X7's value half); a collection operation's static type follows its body's result (#112, closes X8's typing half); a wireDiagnosticcarries itscode(#124); a worked example of the expression forms (#147). - Analysis. A verification case runs and reports its body's verdict beside requirement
satisfaction, and its objective checks the case's subject (#117, #156, closes A6);
-sweep,-samples -seed,%sweep,%samples,RunSweepandrun_sweeprun a case or a calc once per value, typed by the parameter they bind, and table the rows (#118, #157, closes A3); aTradeStudyruns as the library writes it and reports each evaluation, with%optimizerefusing an objective bound to the case's own calc (#133, closes A2); one simulation clock owned by the runtime context,accept after/atin action bodies,Context.Advance, thedue orderchoice point andfinal_timeon the wire (#136, closes A5); a worked walkthrough of analysis cases underexamples/analysis-demo(#148). - Wire and clients. Every value kind above crosses gRPC/Connect under its own capability
(
infinity_value,function_values,set_values,tensor_values,verification_verdicts,case_evaluations,schedule,schedule_explore,final_time,diagnostic_codes), read by the Go, Python, Node, Rust and Java clients; the Go and Python clients add sweeps, exploration, evaluations and the schedule option (#113, #117, #118, #121, #122, #124, #125, #133, #134, #136). - Release, CI and records. The Windows MSI builds again on the GitHub runners and
v0.7.0was the first release to publish one (#127, R4); pull requests run GitHub Actions only while CircleCI runs onmainand tags, as four jobs under the plan's time limit, with the wire-compatibility baseline the merge's own parent or the previous tag (#108, #132); development moved to adevelopintegration branch (#151); the coverage the SonarCloud scan reads measures every suite the checks run (#142) and the findings are cleared (#109, #139); loading a large model merges the library's member set once rather than per declaration (#114); the pre-1.0 rule for which version segment a release bumps is stated inCONTRIBUTING.md(#131); the bounded-model-checking design record (#135, #158) and the bindings track below (#140) are written; the roadmap was refreshed to thev0.6.0baseline and then swept for what landed after it (#111, #149); and the repository's agent skills were extended (#128, #129, #137, #144, #152).
What closed between v0.7.0 and v0.8.0 — 113 merged pull requests (#162–#279, less #216
and #232, which were closed unmerged, and #263, #267 and #278, which merged to develop after the
tag), in CHANGELOG.md's 0.8.0 section for the detail, grouped as the paragraph above is.
- Analysis framework and engines. Every analysis question goes through one framework
(
internal/exec/analysis, #172, #162): each plan runs on a resolver and semantic model of its own (#180), runs and sweep rows go in parallel under-jobswith the same answer whatever the count (#190, #193), a sweep over a held object runs on the object as the session holds it (#215), every verdict states its standing and the engines can be listed and chosen (#182), an action annotatedToolExecutionruns through an external tool (#189), and an engine is a directory of executables speaking over standard input, bounded byOPENSYSML_TOOL_MAX_OUTPUT(#235; design note #171). - Model checking. An SMT model checker for actions on concrete inputs (#185), over free inputs
(#233), deciding whether the schedule decides a feature under its own solver budget (#262,
#279), reading timed accepts, messages and nested flows (#260);
-engine checksearches every schedule of an action, of a state machine, and of the two together, with static reduction and time ties (#192, #244); thereplay:<file>scheduling policy replays a witness, including a clock-retried step (#258); the OMG PSSM state-machine suite runs as an advisory referee (#230); the bounded model checker's independence relation counts message order (the design note, #217's companion). - Expressions and values. The extent operator
all Tevaluates, staticallyT[0..*], over the whole model, and a namespace-level usage of several occurrences denotes its objects (#211, #238, #239, closes X5 and the expression half of Q2);x meta Tevaluates andx.metadataends with the reflective metaobject, which crosses gRPC (#212); a[0..*]feature holding one value denotes that value wherever one value is expected, including through a feature chain, sointerpolateLinearruns (#164, closes X2 and unblocks L7); function values compile natively in C and Go (#165, retiring the compiler's function-value refusal); a literal's direct type is itsScalarValuesdefinition, scale arithmetic agrees with the library, a computed quantity is reported in its dimension's coherent unit, and classification against an enumeration is decided by its values (#205, #191, #179, #208, #177); a bare feature reference is typed statically by the feature it names, a computed value is judged against a scalar-typed feature, and an argument of unknown type keeps every overload (#213, #237, #241); a multi-valued feature not declarednonuniquerefuses a repeated value (#178). - Executor. A deferred event outranks an enclosing transition, a history with nothing to
restore performs the ordinary entry, a composite's completion fires its own completion
transitions, and a
choicereads its guards after the incoming effect (#228); adobehavior may be a full action that waits on the clock (#181); a body paused mid-statement is state a snapshot captures, and the runtime'sContextis a sharedModelplus a run's own state (#187, #259);first a then b;in action and state bodies is the succession it names, with feature-chain ends (#269, #270, #274);%sendreaches an object whose performed action waits at anaccept(#271);entry point/exit point— an extension with no SysML v2 notation — are gone (#184). - Diagrams and documents. A view renders as Graphviz DOT with the
dotform, in the pilot visualizer's Standard B&W style with an optional colourblind-safe palette, and a document renders its diagrams as DOT on request (#222, #248, closes W1 and W3's DOT half); a view renders as PlantUML with theplantumlform and a document as PlantUML on request (#264, closes W2 and W3's PlantUML half); a model can say where a view draws its elements and every rendering carries it, validated by-validate(#227); node labels lead with the name (#243);sysml -renderaccepts several files as one model (#167). - Editing. The VS Code diagram panel is an editable canvas: an Add… menu, connection edits,
a dragged node's position written back, Move to… across namespaces, and rename or delete
following references into the other documents of the model, all through the
opensysml/applyModelEditLSP request overinternal/check/edit(#247, #261, #265, #266). - Library and identity. Named standard-library elements carry the normative element ids the
KerML and SysML specifications assign, and a copy of a library file rooted at the library's
packages converts as the library (#245, #276, #278 — the last past the tag); a
MOSAlibrary and its openness checks (#200); every bundled library file converts back from its graph without source text (#272). - Records, corpora and site. The pinned OMG pilot implementation is release
2026-08(#224); the precise-semantics alignment note (#217), the embedded-target design note (#226), the runtime showcase underexamples/runtime-showcase/(#225), the architecture self-model's analysis flows (#194, #201), the execution-semantics vocabulary (#183) and the landing page's carousel (#203); SysML v1 migration reads the open model formats tool-neutrally (#188); the Python client answersModel.find,Model.getand indexing from the model (#207); the SonarCloud findings are cleared on each pass (#196–#199, #242, #253, #257, #275); the roadmap gained D12, Track W (#195) and A7 (#214) along the way; andmainwas merged back intodevelopafterv0.7.0(#202).
The previous baselines' retirements — native compilation's first phase, the wire contract, L3–L6, the nested-action frames, D3, Q4, the changelog fragments, the census and its gate, the enumeration rules (#907, #909) — stay retired and are not repeated.
Full gate green: gofmt -l . empty, go build ./..., go vet ./..., go test ./..., and the
corpus gates run locally clean at the baseline, with the pilot material re-fetched at its pin
(./scripts/download-pilot-corpora.sh, ./scripts/download-pilot-xpect.sh). A stale local copy
of the pilot corpora fails cmd/pilot-diff, cmd/pilot-xpect and the TestPilotCorpora gate
with a provenance message naming the drift; that is the gate working, not a regression — re-fetch
before re-recording anything.
| Gate | Count at v0.8.0 (238aed650, 2026-09-13, Go 1.25.0); the v0.7.0 figure in brackets where it moved |
|---|---|
| OMG training corpus | 100/100 clean — asserted, not ratcheted: no file reports a semantic error |
| OMG pilot corpora (ratchet) | 213 files; 7 report a diagnostic [6], each adjudicated in pilot-corpora.md and omg-issues.md |
| Stdlib parser conformance | 100/100 clean — 94 vendored OMG files and 6 non-normative OpenSysML extensions |
| Execution conformance cases | 894 under TestExecutionConformance, all run and pass, none skipped [770] |
| Known execution-conformance failures | 0 — known_failures.txt holds no case: "every derived case passes" |
| Cases admitting several outcomes | 26 .expected.json files list outcomes, each citing its derivation in the behavior semantic oracle; the harness explores every one of them under explore [19] |
| Trace partial orders | 8 .trace.order files, each a set of a < b lines the recorded trace must satisfy [5] |
| Golden execution traces | 276 .trace.golden files: 230 under the default schedule (TestExecutionTrace, one per case) and 46 per-policy goldens (<case>.declared, <case>.seed-1) [216: 182 and 34] |
| Runtime robustness cases | 459 first-level subtests of TestRuntimeRobustness [369] |
| gRPC conformance fixtures / robustness cases | 15 / 8 (TestGRPCConformance, TestGRPCRobustness; the authoring service adds 2 robustness cases of its own) |
| Golden AST fixtures | 205 (TestGolden: 177 SysML, 28 KerML) [197: 171 SysML, 26 KerML] |
| Negative parser subtests | 252 first-level subtests of TestNegative (396 across the TestNegative* functions, 454 across every *Negative* parser test) [249: 338 and 396] |
| Rejection oracle | 306 self-authored invalid models: 293 both reject by default and 297 when we are asked strictly, 4 the pilot alone by default and none strictly, 9 ours alone [285: 273 and 276, 3 the pilot alone] (the control-node rules the pilot leaves unimplemented and a non-Boolean succession guard) |
| Validation census | 162 of 217 named constraints reported (156 faithful, 6 approximate), 1 not implemented, 1 deliberate, 53 unknown |
| RDF corpus round trip | 353 of 353 models stable, none refused [346] |
| API element form corpus round trip | 354 of 356 models stable, 2 respelled reals, none refused |
The pilot differential, the Xpect oracle, the scope oracle and the rejection oracle are the
external conformance statement, and their figures are generated into README.md by make docs-counts from the committed baselines; they are not repeated here.
The test-suite figures above are counted from go test -v at the tag. The other surfaces that
repeat them (README.md, docs/project/spec-compliance.md, docs/internals/architecture.md)
were typed in by hand at the tag and lagged it — last recounted at 074f9c4b7 (#245), they read
889 conformance cases where the tag has 894. Since #291, on develop after the tag, they are
generated: tools/cmd/doc-counts counts the conformance cases, golden ASTs, golden traces, negative
parser subtests, runtime and gRPC robustness cases and top-level Test functions from the tree
the way the gates enumerate them (the conformance cases through tests/fixtures, which the
runtime and gRPC conformance tests read too), make docs-counts writes them into marker blocks
beside the refereed pilot figures, and go run -C tools ./cmd/doc-counts -check fails in CI when a block
and the tree disagree, so the surfaces cannot drift from the gate table again. They have since
left git altogether: the site build counts them into the compliance map's test inventory
(scripts/mkdocs_suite_figures.py over go run -C tools ./cmd/doc-counts -site-blocks), the committed
pages name what is counted without a figure, and a branch adding a test rewrites no shared line.
The tests-and-subtests total of a run, which only a run can state, is no longer quoted anywhere. At
develop's head the blocks read 1013 conformance cases, 307 default and 80 per-policy trace
goldens, 493 runtime robustness cases, 21 gRPC conformance and 8 gRPC robustness cases, 208 golden
ASTs and 252 negative subtests; the growth over the table is fixtures landed after the tag, all
unreleased. The robustness cases are counted across the TestRuntimeRobustness* and
TestGRPCRobustness* functions since #345 registered them per feature, so the table's
single-function figure and the block's are the same census under two spellings. The rejection-
oracle row follows the committed baselines and did not move between 074f9c4b7 and the tag. At
develop's head, the validation census has moved to 163 of 217 reported —
157 faithful, 6 approximate, 0 not implemented, 1 deliberate, 53 unknown, and the RDF corpus
round trip has moved to 356 of 356 models stable. The API element-form round trip at the
same head is 354 of 356 models stable, with 2 graph-diff cases.
Statement coverage, re-measured with go test -cover ./... at this baseline with the corpora
present. It counts only each package's own tests, which understates a package consumed by others
(internal/syntax/ast is exercised by every parser test; internal/translate/codegen's differential
runs from internal/frontend/repl). It is not the figure the SonarCloud scan reads: since #142 make coverage also builds the sysml, sysml-grpc and sysml-lsp binaries instrumented and folds
the counters their test runs write into coverage.txt, so the scan's per-package figures for the
command packages are higher than these.
| Package | Coverage | Package | Coverage |
|---|---|---|---|
internal/core/quickfix |
100.0% | internal/semantic/symbols |
84.7% |
internal/core/conformance |
100.0% | internal/exec/solve |
84.3% |
internal/syntax/ast/astcodec |
99.5% | client/opensysml |
84.2% |
internal/syntax/format |
97.2% | internal/semantic/identity |
83.5% |
internal/doc/docrender |
94.4% | internal/workspace/project |
81.0% |
internal/translate/rdf/ontology |
92.1% | internal/doc/queryexec |
80.9% |
internal/frontend/grpc |
90.9% | internal/semantic/resolve |
76.3% |
internal/workspace/libs |
90.0% | internal/semantic/query |
73.2% |
internal/check/passes |
89.9% | internal/workspace/model |
73.0% |
internal/syntax/parser |
89.8% | internal/ir/lower |
71.3% |
internal/translate/export |
89.3% | cmd/sysml-lsp |
70.0% |
internal/translate/migrate |
89.1% | internal/semantic/semantics |
64.3% |
internal/frontend/lsp |
88.3% | cmd/pilot-exec-diff |
41.6% (the pilot-evaluator half is gated) |
internal/frontend/repl |
88.2% | internal/translate/interop/flexo |
39.5% (the live-stack half is gated) |
internal/exec/runtime |
88.1% | cmd/sysml-grpc |
38.7% |
internal/translate/rdf |
87.1% | internal/syntax/ast |
21.8% |
internal/ir/queryplan |
85.6% | cmd/sysml |
17.8% |
internal/doc/docir |
85.6% | internal/translate/codegen |
1.9% |
The corpus gate needs the corpus (./scripts/download-training-examples.sh) and never
re-baseline internal/workspace/model/testdata/training_examples_expected.txt: adjudicate each
drifted file and record the verdict in docs/project/training-examples.md.
A tag cannot be cut over a corpus regression. Pull requests run one CI, the GitHub Actions
workflow .github/workflows/pr.yml, which downloads the corpora (cached on the download scripts)
and runs the suite with OPENSYSML_REQUIRE_TRAINING_CORPUS=1, OPENSYSML_REQUIRE_PILOT_CORPORA=1
and OPENSYSML_REQUIRE_SMT=1 before a merge, with the protobuf wire-compatibility check against
the branch the pull request merges into (#108). .circleci/config.yml runs on main and on v*
tags only, as four parallel jobs since #132 — Go static checks, Go race tests, Go coverage profile and Go gates and binaries — each downloading the corpora it needs and requiring them
the same way; the client tests, the SonarCloud scan and every release workflow wait on all four.
Its wire-compatibility baseline is the merge's own first parent (HEAD^1) on main and the
previous release tag on a tag build, so the verdict cannot change with what merges afterwards
(#132).
Tagging a core release, publishing the Python client and the Homebrew bump are all proven paths:
v0.8.0 is tagged and its release workflow completed with the full archive set, as v0.7.0,
v0.6.0, v0.4.2 and v0.4.3 did before it; opensysml-v0.5.0, tagged with v0.8.0, uploaded
the client to PyPI as opensysml-v0.4.0 did before it (its release-python workflow needed a
rerun after two Go coverage profile failures, and passed); and the tap
Open-MBEE/homebrew-tap bumps itself from its own scheduled workflow on
each tag, rendering the formula from this repository's scripts/render-homebrew-formula.sh and
template. The procedure and its post-tag verification are in docs/project/releasing.md.
CHANGELOG.md's 0.4.3 section holds what 99e02003 shipped (#877), its 0.5.0 section
what 0fdeb11e shipped (#894), its 0.5.1 section what d7b3eb45 shipped, its 0.6.0
section what 30f103bb9 shipped (#988), its 0.7.0 section what e0fbfea5b shipped (#145,
#154, #159) and its 0.8.0 section what 238aed650 shipped (#268, #277), each dated to its
tag; Unreleased is empty in the file because since #852 a change adds a fragment under
changes/unreleased/ instead, and python3 scripts/changelog.py release X.Y.Z folds the
fragments in at release time (releasing.md). The 0.5.0 fold was the first release cut that way
and every release since followed it; under git-flow the fold rides the release/x.y.z pull
request into main and returns to develop with the back-merge (#202 after v0.7.0, #286
after v0.8.0). Nothing remains here; the item is kept because the folds are the procedure's
proof.
Each has its release workflow (client-node-v* to npm, opensysml-java-v* to Maven Central,
opensysml-rust-v* to crates.io) and a worked example the tests run, and none has ever been
tagged. The Java package name already moved to org.openmbee.opensysml, the DNS-verified
namespace, so nothing blocks Maven Central but the account. npm and crates.io need a publisher
token in CI; Maven Central needs the Sonatype account and a signing key. These are account gates
like R4, not engineering.
Everything about the tap is automated and verified on Linux (install, brew test,
brew audit --strict --online), and the manual pages (#699, packaging/man/man1/*.1, generated from
internal/frontend/usage and drift-gated by make man-check) are in the bundles the formula installs. The
one thing never done is running the darwin bottle on macOS: the darwin archives' checksums match
the release manifest and nothing more.
homebrew/core — which would drop the tap and the trust step entirely — is gated on
notability (75 stars / 30 forks /
30 watchers, or 225 / 90 / 90 self-submitted), so it is not a near-term option.
macOS binaries are not Developer ID signed or notarized, so a browser download trips
Gatekeeper. Root-caused in docs/project/macos-distribution.md: it is com.apple.quarantine,
not a missing signature — Go's linker already ad-hoc signs darwin/arm64 — so ad-hoc codesign
in CI would change nothing. Notarization needs an Apple Developer account, a Developer ID
certificate, an App Store Connect API key in CI and a macOS runner: a purchase, not a task.
Windows Authenticode signing is prepared, pending approval by
SignPath Foundation, which signs open-source Windows binaries for free
from a build it can verify the origin of. The repository side is done: the README carries the
Code signing policy the terms require, the three Windows
executables embed the VERSIONINFO SignPath enforces (ProductName OpenSysML,
ProductVersion from the tag), and .github/workflows/release-windows.yml rebuilds them on a
v* tag under GitHub Actions — a trusted build system for SignPath, which CircleCI is not —
submits them for signing, and publishes the signed files as *-signed* release assets beside
the unsigned ones that the cosign-signed manifest keeps covering. What remains is a
maintainer's application at https://signpath.org/apply, the SignPath project and policy
setup, the SIGNPATH_* secret and variables in GitHub, and one manual approval per release;
until then the workflow builds and stops. Procedure: docs/project/releasing.md, "Windows
Authenticode signing".
Windows packaging is in place on the same workflow: a WiX v5 MSI (packaging/msi,
scripts/build-msi.sh) installing the three executables to Program Files\OpenSysML on PATH,
with the Z3 solver as an optional feature pinned by hash, published unsigned as
opensysml-<x.y.z>-windows-amd64.msi. v0.6.0 shipped no .msi: its msi job failed with
error: C:\Program is required, because the script read the wix command from the WIX
environment variable, which the preinstalled WiX v3 on windows-latest exports as its
installation directory. #127 renamed the override to WIX_CMD, and v0.7.0 and v0.8.0 each
published their .msi. Those two ran as Windows Installer's bare progress window; #350, on
develop after the tag, gives the MSI a setup wizard — destination folder, the gRPC service
and Z3 as selectable components, a confirmation and a completion page, repair and remove on a
second run — so the next tag's .msi is the first a user is walked through.
Once SignPath is configured it is rebuilt from the signed executables and itself signed as
*-signed.msi (Z3 stays unsigned by SignPath's terms). Scoop,
winget and MSYS2 manifests that depend on Z3 rather than bundle it are maintained as templates
under packaging/ with render scripts; what remains there is a maintainer submitting each to
its external repository (and, for winget, confirming the OpenMBEE.OpenSysML identifier and a
Z3 package to depend on). Procedure: docs/project/releasing.md, "The Windows installer".
editors/vscode builds only as a PR CI artifact: no .vsix is attached to a release and there
is no marketplace or Open VSX listing, so a user cannot install it without building it. The
client works against the shipped sysml-lsp (--stdio is accepted and shutdown/exit are
honoured). What remains is packaging and publishing: vsce package in the release workflow, a
.vsix on the release, and (for the marketplace) a publisher account and a PAT in CI — the same
class of account gate as R4.
Filed and waiting on the other side: the identity-annotation enhancement against SysML 2.0
(INBOX-2510, maintainer-approved 2026-09-01). Filed against the pilot and fixed there: the
ownedDisjoining EMF defect (SysML-v2-Pilot-Implementation#790, shipped in 2026-08) and the
cross-subsetting validator throw (#794). Drafted and waiting on a maintainer to authorise
posting: the three dimensional-analysis errata in the pilot's example corpus, the question about
the queryx/failing Xpect fixtures, the unenforced control-node constraints, and the nine
dimension defects in the published SI.sysml and USCustomaryUnits.sysml — which #304, on
develop after the tag, put under the declared errata overlay, so the bundled library reads with
the three that have one correction applied and the six that do not documented, the vendored bytes
untouched. All are in omg-issues.md, body and status; none needs code here until
an answer arrives.
Loading is closed: every library file is parsed and indexed on every load path, built once and
frozen, read through a per-model overlay (libs.Loader, libs.SharedBase, symbols.NewOverlay)
rather than copied per model, and the serialized snapshot brings a process up in under 20 ms.
Name resolution is closed too: a qualified name through a public import (ISQ::speed or any
user façade package) evaluates (#801), an unqualified library call resolves at evaluation exactly
as the checker resolved it (#816), and an overloaded name reaches the declaration the argument
types select (#825, #861). The library review after v0.4.3 measured evaluation against the
Kernel Function Library's 279 declarations in 17 packages and against the domain libraries; of its
findings L3–L6 have landed and L7 — the analysis libraries — is what remains, and it is a Track X
item in library clothing.
A feature a user type inherits from the standard library has no value at run time. With part def Box :> Item; part b : Box;, both b.isSolid (declared isSolid = isEmpty(voids) in Systems Library/Items.sysml) and b.voids report member … not found in instance; the cause is
runtime/shape.go skipping every libraryDeclared feature when it lays out an object, so
Model.Eval never gets as far as folding the library's value expression. Landed:
PR #830 materializes Systems and Domain library
features on objects in tiers (a feature's own value, then inherited value expressions); at this
baseline %instantiate L::b; %eval in b : isSolid answers true and voids []. The solver has
the mirror gap and is not in that PR: its translatable subset does not take library-declared
conditions, and solve's
differential harness still indexes the standard library as ordinary documents (parseLibraries)
to reach them. lossless-library-records.md records
what was measured.
At the previous baseline ~135 runtime names were registered explicitly (runtime/builtin_names.go,
library_functions.go), operators and the core builtins were handled separately, and 12 of the
17 KFL packages had no dispatch gate: a call to one of their ~38 named functions — every
ToString/ToInteger/ToReal/… conversion in BaseFunctions/ScalarFunctions, Rational floor/round/gcd, sum0, the generic max/min, the explicit operator-call spellings '+'(a, b)
— fell through to no result expression naming the wrong thing, instead of either computing or
failing as library function X is not implemented. Landed:
PR #821 adds the gate and implementations for
every KFL declaration; a call now either computes the library's stated result or is refused by
name (ToString(42) is "42", max(1, 2) is 2, max(1.0, 2.5) is 2.5 at this baseline).
The named-reducer forms (->reduce '+') are the same mechanism read from a different position and
are X5 below; which overload a name reaches is L6.
QuantityCalculations (28 declarations) had no runtime registration at the previous baseline, and
a model that imported it lost ->sum() over quantities (measured; the import brought a sum
declaration nothing dispatched into scope ahead of the scalar one). Composed units had no canonical
rendering. Landed: PR #818 dispatches the
package and renders composed units canonically — with the import in scope, (2 [kg], 3 [kg])->sum()
is 5 [kg] and 2 [m] * 3 [m] is 6 [m**2] at this baseline. What the same probe shows is L6's
gap: with ScalarFunctions and QuantityCalculations both imported, ToReal("2.5") reaches
QuantityCalculations::ToReal and is refused for its argument type instead of selecting the
scalar overload. Domain failures (sqrt of a negative quantity, a unit mismatch in +) must stay
typed errors naming the function and the units, as the scalar library's already do.
The checker and the runtime used to pick a library overload by name and arity, so max(1, 2) and
max(1.0, 2.0) reached the same declaration and the result type was whatever that declaration
said. Landed: PR #825 selects by argument
type with one selection shared between semantics and runtime, and
PR #861 makes every reader of a call —
expression calls, document queries, send — use it; PR #862
keeps a calc that specializes a library function on its own signature. The ToReal("2.5") probe
under L5 reaches the scalar overload. On an ill-formed model the runtime package now agrees with
the checker about a named argument it cannot place (#904, #908): the label is kept as written and
refused as ErrUnknownParameter rather than bound by its last segment.
What runs at this baseline, against bin/sysml: a domain library's calc executes from its own
text as a model's does (#133), so SampledFunctions::Sample(Sq, (1.0, 2.0, 3.0)) with Sq a
user calc def samples it and constructs the SampledFunction (X6, #122 — at v0.6.0 this was
refused cannot evaluate definition Sq), Domain(sampled) reads [1.0, 2.0, 3.0] off it, and
TradeStudies::TradeStudy runs as the library writes it — evaluationFunction bound as a
function value, MinimizeObjective/MaximizeObjective computing best over the alternatives,
selectedAlternative found by the inherited ->selectOne (A2); and since #164
interpolateLinear(sampled, 1.5) runs inside the library's own Linear — its f subtracts
lowerSample.domainValue, a singleton read through a feature chain on an object, which at
v0.6.0 arrived as the sequence [1.0] and was refused operator '-' is not defined for a Real and a sequence. That was the chain-read half of X2, and it is closed: a [0..*] feature holding
one value denotes that value wherever one value is taken, read off the feature itself or through
a chain, and the conformance case calc_sample_pair_arithmetic pins the arithmetic over
SamplePair::domainValue/rangeValue and the interpolation (5.0). spec-compliance.md no
longer lists a SampledFunctions row as not implemented. StateSpaceRepresentation depends on
A4. The vector half moved earlier: since #883 a NumericalVector and a VectorQuantity are
value kinds of their own, VectorFunctions computes over them and OccurrenceFunctions
evaluates over lifetimes (#884), the 0.6.0 release added the tensor quantity, coordinate-frame
and measurement-scale values the measurement reference libraries declare, and #121 added the set
kind and tensor quantities of any rank, so no value kind the analysis libraries declare is
missing. The measured per-library conformance table — package → declarations → evaluated →
refused by name → wrong — landed in #292 on develop after the tag, so the library's status
stops being anecdotal: TestAnalysisLibraryCensus (internal/exec/runtime) enumerates every
public callable declaration of the six packages, invokes each through the runtime against a
representative model and records whether the value passed its check, which typed error refused
it, or what the value got wrong; the verdicts are committed to
docs/project/analysis-library-census.json, make docs-counts renders them as the table in
spec-compliance.md with every refusal listed by name and error, and go run -C tools ./cmd/doc-counts -check fails when the table and the file disagree. At develop's head it reads 76 declarations,
53 evaluated, 23 refused by name, 0 wrong: SampledFunctions 5 of 5, TradeStudies 6 of 7,
VectorFunctions 30 of 39, OccurrenceFunctions 6 of 8, StateSpaceRepresentation 6 of 17
(4 before A4 landed in #296; what stays refused there is the abstract protocol probed directly —
getNextState, getOutput, getDerivative and getDifference are undetermined until a model
specializes them, and Integrate, the two event definitions and StateSpaceDynamics itself
have no body of their own to run), and AnalysisTooling declares nothing callable. Nothing is
left in the track; a verdict that moves is adjudicated in the change that moves it.
ShapeItems defines a shape's faces, edges and vertices through bind connectors with
multiplicities, and what those determine now evaluates. A Box answers faces (six), the
per-face edges (four each, with length/width), edges (twenty-four) and the per-face
vertices (eight); a Cylinder or Cone answers faces (three or two) and each Disc's
edges, the ellipse; a Cylinder nested as a Box's voids answers the same and makes isSolid
false. What made the curved shapes fail was the runtime reading binding [1] bind [0..*] base.edges = [0..*] be as a whole binding, so be [2] was bound to the one edge and every read
through it was ErrMultiplicityViolation; a connector's own multiplicity is the number of links
it declares, so that binding is partial — it relates the disc's edge to some value of be — and
runtime/binding.go partialBinding now decides so from the declared bounds (lower/binding.go
Binding.Multiplicity). What stays a typed error is what the library leaves open, not a runtime
gap: the [0..1]-bound edge and vertex groups (tfe, tflv, and so Box::vertices), which no
binding pins to a member, are ErrBindingEnd naming the binding; be/ae and Cylinder::edges/
vertices reach cf : Surface, whose edges/vertices are the Kernel frame's, not the object's;
and matingOccurrences/spaceBoundary are frame features of Occurrences.kerml. See the
ShapeItems rows and Known Limitations in spec-compliance.md and the record in omg-issues.md.
The interpreter in internal/exec/runtime is the reference semantics and is fast in absolute terms
(about a microsecond per calc invocation after the September pass), but a compute-bound analysis —
a recursive calc, a long numeric loop — is still three orders of magnitude off native code. The
goal is that a calc, and eventually a whole analysis case, can be compiled ahead of time into a
standalone native program that computes exactly what sysml -calc computes, prints it the same
way and fails on the same inputs, or refuses to compile with a typed error naming the construct
outside the subset. Nothing is compiled approximately.
sysml model.sysml -compile Pkg::Fib -o fib translates a calc def (or a calc usage) into an
executable through C (cc -O3 -flto, the default) or Go, with -source writing the generated
source alone; the design, the measurements and the phase plan are in
native-compilation.md. The pipeline is parser → resolve → semantics
plus lower.CalcBody into codegen.Compiler, a typed IR (codegen.Program), and
EmitC/EmitGo. Landed in four pull requests: #778 the scalar subset (Integer/Natural/
Positive/Real/Boolean parameters, literals, checked arithmetic and comparison, the logical
and conditional operators, body-local attributes and assignment, if/while/loop … until,
direct and mutual recursion); #796 statement bodies, redefined parameters, the scalar library
intrinsics and named arguments; #828 homogeneous sequences of the scalar types with any
multiplicity, the shape rules, for, the sequence and control libraries and the element budget;
and, on the interpreter side, #781's closure fast path for pure calc bodies, which is the tier
between interpreting and compiling. Everything outside the subset refuses with
codegen.UnsupportedError naming the construct; a differential test runs every compiled program
against the interpreter in both backends, and the gate stays green with cc absent (the C tests
skip; the Go ones do not).
Measured (Xeon 8559C, GCC 11.4, 2026-09-02): Fib(25) 261 ms interpreted, 919 µs as Go, 221 µs
as C; SumTo(1000000) 1216 ms / 764 µs / 379 µs; Collatz(27) 206 µs / 5.2 µs / 0.98 µs. C beats
Go by 2–5× on every loop or recursion, which is what justified keeping two backends: C is the
default and Go the fallback where no C compiler is installed.
native-compilation.md states the target as a dependency-free executable or a library with a small C API that runs a whole model, in seven phases: values, instances, constraints and requirements, documents, actions, state machines, embedding. Phase 1 is half done (the collection half above). In the order they matter:
- Records, enums and record field access — the remainder of phase 1.
type X is not Integer, Real or Booleanis the refusal today, as is a sequence mixingIntegerandRealelements, and the interpreter's newArray/NumericalVector/VectorQuantitykinds (#883) are refused by the compiler by the same rule until they have a native layout. Quantities with units and structured parameters follow from the record type. This is what stands between "a calc compiles" and "an analysis case compiles" (A1). - The step budget. The interpreter stops a runaway loop at
OPENSYSML_MAX_STEPS; compiled code counts elements (the element budget landed with #828) but not steps, so awhilethat never terminates runs forever. Decide whether a compiled program carries an optional iteration counter or whether "a compiled program is a program" is the documented contract; N2.4 and M1 both wait on the answer. - The rest of the library. What L4 registers in the interpreter needs C and Go equivalents with the interpreter's exact results and failure behaviour (domain errors, Integer range) as each package is reached; the interpreted/compiled differential is the gate.
- A stable C ABI. One entry point per compiled calc — typed arguments in, a typed result or a
typed failure out, no allocation the caller does not control — would let the REPL and
sysml-grpccall a compiled calc in place of interpreting it, is what I4 (the C client) links against, and is what M4 (the embedded host interface) is a restriction of. Design it once for all three; not before N2.2 is decided, since a linked-in calc must honour the host's budget. - Packaging. The C backend needs GNU C (
__int128,__builtin_*_overflow,setjmp/longjmp); document the toolchain requirement in the install guide, and decide whether the release bundles should carry a prebuilt runtime shim. - Phases 2–7 — instances, constraints, documents, actions, state machines, embedding — in the record's order. Actions and state machines are token and event semantics, not arithmetic, and the interpreter's trace is their contract; their closed IR is Track M's first item (M1), so the two tracks meet there rather than duplicating the work.
Saving and SysML ↔ RDF Turtle conversion landed (internal/translate/rdf,
internal/translate/export, %save, sysml -convert, -sync-diff); see
the RDF mapping.
The RDF direction ships experimental, because of D1 and D2 below: its vocabulary
may change without a compatibility path, and the one triplestore interop measured — Flexo — still
drops what those items carry. Every surface says so (convert.ExperimentalNotice), and promoting
it to stable is re-measuring the harness once those land, not a documentation change.
Measured by the per-file ratchet at this baseline (TestCorpusRoundTrip,
internal/translate/export/testdata/corpus_roundtrip_expected.txt), all 353 models under
examples/ convert (the training corpus, the three pilot corpora and this repository's own
demos), every one round-tripping notation → RDF → notation → RDF byte-identically; there is no
whitespace-only, graph-diff, unwritable, unparseable or refused verdict left — each file is
pinned, so a movement in any direction has to be adjudicated
(rdf-corpus-roundtrip.md). A model the mapping cannot write back is
still refused rather than converted lossily; the corpus simply no longer contains one. The
previous baseline's last refusal class — 40 declarations naming no element of their own (anonymous
feature, event, snapshot, timeslice and assert) — went in 0.6.0: the graph types the fact
the keyword states (sysml:portionKind, sysml:EventOccurrenceUsage, sysml:AssertConstraintUsage
with sysml:isNegated, the named occurrence or constraint as sysml:references) and records
sysx:declaredKeyword, so the decoder spells the head from the typed facts, and a graph whose
keyword contradicts its typing is refused naming the element. Before that, the 13
result-expression refusals went with #815 and #835 (a body's trailing a - b is a
ResultExpressionMembership owning the expression itself), the 19 prefix-metadata refusals with
#824 (metadata bodies and prefix metadata are owned MetadataUsages), and the
duplicate-declaration refusals once the parser read the anonymous connector a to b; as ends
rather than as a connector named a. #827 spells a reference back so it re-resolves to the
element the graph named and #855 links a reference reached through an import or alias. Nothing is
in flight against these numbers.
The goal is that a graph OpenSysML writes can stand in for the RDF
flexo-mms-sysmlv2 produces: loaded straight into flexo-mms-layer1-service as a
branch's model graph, and read back through the SysML v2 API surface as the same
elements, without that service having produced it. Two consequences shape D1–D3, and
both were read from the two services' sources rather than from our own docs:
- Layer 1 imposes no vocabulary at all.
routes/gsp/ModelLoad.ktloads whatever triples the request body carries into a load graph, diffs it against staging and commits; sanitization (sanitizeCrudObject) applies to LDP CRUD objects — orgs, repos, branches, policies — not to model triples. So layer 1's requirements are transport-level: a Turtle body onPUT .../branches/{branch}/graph(or SPARQL update on.../update), the ETag precondition, an optional?message=, and the literal-size limit (maximumLiteralSizeKib). Named-graph layout, commits, locks and provenance are layer 1's own and are not ours to emit. - The vocabulary contract belongs to the reader in
flexo-mms-sysmlv2.ElementApi.extractModelElementToJsonis what turns triples back into API payloads, and it is stricter thanNamespaces.ktsuggested. It keepssysml:andurn:sysmlv2:annotation:json:predicates and ignores everything else (the unrecognized-predicate error is commented out), so everysysx:triple is dropped the moment a graph passes through that service. Whatever a model needs in order to survive has to be standard, which is what makes D1 and D2 part of the interop goal rather than refinements after it.
What matches today: sysml: = https://www.omg.org/spec/SysML# and elmt: =
urn:sysmlv2:element: are identical to Namespaces.kt; rdf:type plus sysml:<property> per
scalar field is the shape the reader expects; our typed literals fall in the datatypes it maps;
every element carries sysml:elementId equal to the id its IRI ends in, so listing by id and the
@id derivation agree; and ownership is materialized as the abstract syntax states it, so the
roots endpoint sees one root per document. Those were D3.1–D3.3 and D3.5, closed and now measured
rather than claimed — see the next section.
The harness is internal/translate/interop/flexo, the FLEXO_INTEROP gate TestFlexoInterop, documented
in .agents/skills/flexo-interop; it brings up the published openmbee/* images, PUTs our
Turtle to a branch graph, reads every element back through flexo-mms-sysmlv2 and compares with
what the service's own commit path stores for the same model. It measures the gap instead of
asserting the fix, so every item below shows up as movement in
internal/translate/interop/flexo/testdata/interop_expected.txt. Keep it out of go test ./....
What the current recording measures, for the identity-carrying fixture: 59 of 59 elements
listed and 505 of 582 properties delivered on the graph-load side, against 33 of 33 and 158 of
158 for the same model posted through the service's own commit path; 1 of 59 reads as a root, and
1 has no owner in the model (expression nodes and connector ends are owned elements now); every
element is readable directly by id; no subject of the graph is outside the element namespace.
Every standard property is delivered, the 26 multi-valued ones included — connectorEnd,
ownedEndFeature, chainingFeature, relatedElement, ownedRelationship (6/6), ownedMembership
(4/4), ownedFeature, ownedFeatureMembership, ownedMember (3/3 each), specializes (1/1) —
since D3.4 landed. The 77 lost properties are one thing:
- The remaining loss is annotation vocabulary in
sysx:— seven predicates: source text and tail, notation spelling (declaredKeyword,endForm,sourceLanguage), body presence and member order.sysx:argumentIndex,sysx:endIndexandsysx:relatedFeatureno longer appear, because operand order and end targets are standardsysml:structure; the remainingsysx:annotations are intentionally documented in D1 and D2 below.
The commit path delivers 6 of 6 of its own multi-valued properties, because it stores each array
whole as a JSON annotation literal alongside the typed triples; the graph now carries the same
literal, and the two paths deliver every array alike. Two deployed behaviours differ
from the sources: the element listing ignores pageSize/pageAfter and returns every subject,
and project delete is a soft annotation that leaves the Layer 1 branch behind.
The reader skips a sysml: predicate with more than one object and prefers the
annotation literal at urn:sysmlv2:annotation:json:<key>, which it parses as JSON.
Anything multi-valued emitted as bare repeated triples alone is silently dropped on read.
PR #850 closed it, and D3 with it: one
json:<key> literal per multi-valued sysml: property beside the typed triples (shape taken from
the service's CommitApi.kt, cited in rdf-mapping.md § Collections), a decoder that accepts
either spelling or both and refuses a graph whose two spellings disagree, and reposync keeping
the literal in step when it mints ids. Re-recorded against the live stack, the multi-valued
standard properties went from 0 of 14 to 14 of 14 delivered, and the total from 355/424 to 369/452
(the denominator moved with the source-text properties the mapping added since the previous
recording); the recording after D1 and D2 is 505/582, with the standard expression and end
structure delivered in full and the remaining source and ordering limitations recorded in D1 and
D2.
Landed in #509.
Every expression-valued position — a feature value, a multiplicity bound, a guard, a filter, a
condition, a send payload — is now a tree of typed nodes in the expr: namespace
(rdf-mapping.md § Expressions): standard metaclasses (OperatorExpression,
FeatureReferenceExpression, LiteralRational, …), roots owned through
OwningMembership/FeatureValue, and operands owned through ParameterMembership, an in
Feature and its FeatureValue. The encoder writes sysml:operator, and a decoder reads a
foreign tree from its structure alone, without requiring sysx:sourceText. SPARQL can see inside
a value now; "every part whose mass exceeds 1000" is expressible.
What remains:
- named-argument redefinition remains represented by
sysx:argumentName, because the metamodel has no notation-level name on an argument occurrence; - source spelling and expression-body ordering remain annotations where the metamodel has no corresponding property; and
an expression standing as a body member— done: a calc's trailing result expression is aResultExpressionMembershipowning the expression (#815, #835, rdf-mapping.md § Result expressions), and no file in the ratchet is refused for an expression.
Landed in #509.
connect, bind, flow, succession and related heads now state their ends through
sysml:connectorEnd, EndFeatureMembership and end ReferenceUsages marked with
sysml:isEnd, whose targets use owned ReferenceSubsetting relationships. Chained targets use an owned Feature and ordered
sysml:chainingFeature; binary connectors additionally carry sysml:sourceFeature and
sysml:targetFeature, while sysml:relatedFeature identifies related features at every arity.
TransitionUsage endpoints use sysml:source and sysml:target. The decoder still accepts the
earlier sysx: end shape and legacy transition predicates.
What remains:
- non-name end targets are retained as typed
sysx:Expressionliterals rather than being fabricated as feature IRIs; and - the
ReferenceSubsettingrelationship is emitted for end targets, whilesysml:referencesremains accepted only for importing interim graphs.
The reader turns a resource-valued object into {"@id": …} and a literal into a string, so a
property the API defines as a reference has to be an element IRI in the graph. imports.golden.ttl
showed both halves of this gap: sysml:importedNamespace "ISQ" was a string where the API expects
a reference, and the metaclass was sysml:Import, which is abstract in KerML — the API's own
elements are NamespaceImport or MembershipImport.
Landed. A reference-valued property links the element its name resolves to, wherever that
element has an identity: an element of the graph by its own id, and a standard library element by
its normative id (D12) whether or not the library is in the graph — attribute mass : MassValue
links <urn:sysmlv2:element:9cd0e404-…>, an implied first start then a links the start
the action inherits from Actions::Action, and import ISQ::MassValue is a
sysml:MembershipImport whose sysml:importedMembership is the normative owning membership,
the metamodel's range. The converted properties are type on every usage, importedNamespace
(now on NamespaceImport only) and importedMembership, sourceFeature on SuccessionAsUsage
and sysx:InitialNode, referent, targetFeature and function. The literal is kept only for
a name that resolves to nothing the model declares (attribute t : Missing::Kind,
->collect without ControlFunctions in scope) and for a body parameter, which is no element of
the graph. The decoder reads both forms — a link or the legacy literal — and spells a link back
by the shortest name that resolves to that element from where it is written, so a graph from an
older release still converts and gains the links on its next hop.
Measured against the OWL ontology (D8) by TestGoldenGraphsMatchOntology (tests/export),
which checks every SysML-namespace triple in the 58 golden graphs against the metamodel's
declared domain and range: 464 triples in 79 distinct metaclass/property violations before,
411 triples in 76 after, with domain-mismatch Import importedNamespace and
literal-for-object-property … sourceFeature on SuccessionAsUsage and sysx:InitialNode off
the inventory in tests/export/testdata/ontology-known-violations.txt. The type, referent,
function and targetFeature keys stay listed for the fixtures' unresolvable names and body
parameters above; the parameters go when D1/D2 make expression bodies elements of the graph.
The abstract-metaclass half is not mechanizable from the ontology: SysML.owl records no ecore
abstractness (see D8), so every metaclass the encoder writes (kinds.go and the constants in
rdf_out.go/rdf_expr.go) was checked by hand against the abstract classes of the pilot's
SysML.ecore and kerml.ecore — ConnectorAsUsage, ControlNode, Element, Expose,
Import, InstantiationExpression, LoopActionUsage, Relationship. Two were written:
Import, now NamespaceImport / MembershipImport and, for an expose, NamespaceExpose /
MembershipExpose in place of an sysx:isExpose flag, and ConnectorAsUsage for a KerML
connector, now Connector. The decoder still accepts both abstract classes from older graphs.
The ratchets did not move: every model under examples/ still round-trips, and the Flexo hop
still delivers every type, referent and targetFeature of its fixture.
Open-MBEE/sysmlv2-rdf-ontology renders the
OMG metamodel (version 202407, from SysML.ecore) as OML and OWL: SysML.owl, 172 classes, 348
object properties, 63 datatype properties, with rdfs:domain/rdfs:range on each. It uses the
same namespace we do and its class IRIs are the plain metaclass names we already emit; the
difference is the properties, each qualified by the metaclass that defines it
(sysml:Element_declaredName, sysml:Element_owner with range OwningMembership), and a
conformant instance graph therefore materializes the abstract syntax's relationship elements
rather than collapsing them.
So this is a second profile selected by a flag, not a superset: the property IRIs differ, so
one graph cannot satisfy both conventions, and Flexo's convention stays the default. The encoder
already separates the term layer (rdf.SysMLTerm, internal/translate/rdf/vocab.go) from the
structural decisions (internal/translate/export/rdf_out.go), so the profile is mostly a term-mapping
layer: property name → defining metaclass.
Done: the table and the gate. internal/translate/rdf/ontology holds the term table generated
from SysML.owl by tools/gen/ontology from a local checkout (version 202407,
upstream commit in the generated header): 411 properties spanning only 336 distinct unqualified
names — 59 names are declared by more than one metaclass (type, value, source, target,
…), so the unqualified convention is genuinely lossy in the other direction and a profile encoder
has to pick by the subject's metaclass (LookupProperty returns every declaration;
AmbiguousNames reports the set). The gate is TestGoldenGraphsMatchOntology, whose inventory is
also the profile's work list, sorted into five causes: properties the metamodel declares on a
relationship or membership element that we collapse into the element (value → FeatureValue,
the multiplicity bounds → MultiplicityRange, isNegated → Invariant, a transition's ends →
Connector) — the same collapse D3.3 undid for ownership and D1/D2 will undo for expressions and
ends; names as literals (D7); metaclass names the 202407 rendering does not have (FlowUsage,
which it calls FlowConnectionUsage, and TerminateActionUsage); the metaclasses of our own
sysx: namespace; and the properties we write into the SysML namespace that no metaclass
declares, each either a relationship the metamodel reifies as an element (specializes,
subsets, redefines, references, aliasedElement, via) or a notation flag with no
metamodel property (isAccept, isResult, isSnapshot, isTimeslice, isChain) — arguably
those belong in sysx: regardless of this item.
Not landed; no open pull request: PR #774
on the previous repository (JPL-Devin/OpenSysML), which the v0.6.0 baseline reported as open
with conflicts, is closed there unmerged, and no equivalent pull request has been opened on
Open-MBEE/OpenSysML. Nothing under internal/translate/rdf/ontology changed between v0.6.0 and
this baseline other than #142's coverage wiring, and the tree has no ontology/sysmlv2/
directory, no cmd/ontology-modules and no make ontology-modules-check: the modules below
describe that pull request's content, which would have to be re-proposed against the current
main to land. It shipped the ontology itself as 41 leaf Turtle modules and 6 layer
ontologies under ontology/sysmlv2/, cut along
the package hierarchy of the normative KerML/SysML XMI (KerML/Root/Elements.ttl,
KerML/Kernel/Expressions.ttl, SysML/Systems/Requirements.ttl, …) with a catalog.tsv from
term to declaring module and owl:imports computed from use. The generator
(cmd/ontology-modules, internal/translate/rdf/ontology/modules) errors rather than dropping data —
every source triple lands in exactly one module and the union is graph-isomorphic to upstream —
and make ontology-modules-check in CI keeps the committed output equal to the pinned sources
(scripts/download-ontology-sources.sh). It is additive to the term table and the Flexo export;
once merged, the profile can import the module a metaclass lives in rather than the monolith, and
a consumer wanting only, say, the requirements vocabulary has a file to import.
Not started: the profile plumbing itself — about one session once the table, the gate and
the modules exist. Conformance beyond that is gated on D1 and D2 rather than on this item: sysx:
has no place in the ontology, so an ontology-profile graph is conformant only as far as those
have landed, and the profile's documentation should say so.
What exists at the baseline, all landed: -sync-diff <repo.ttl | endpoint> computes an
identity-keyed change set between the model and a repository branch, reading the branch through
the SysML v2 API when given an endpoint and never writing; -sync-apply <endpoint> writes that
change set as one commit or a batched series through flexo-mms-sysmlv2, records the commit in
the sync state, refuses a set it cannot apply whole and leaves what did land committed (#791,
internal/translate/interop/reposync, internal/translate/interop/flexo); and the harness above loads a whole
converted graph into a Layer 1 branch by PUT and measures what the service reads back. Both
directions are refereed against the live stack, and both are element-keyed, which is what identity
bought.
The round trip a modeller expects from a repository is now in place; each piece landed small now that D3.4 is in:
- Read a branch as notation — landed.
sysml <branch-url> -convert sysml(orttl) reads a Flexo MMS project branch as its head commit's RDF graph — the read-sync-diffmakes — and converts it throughconvert.FromGraph. The URL is the SysML v2 branch resource (http(s)://host[:port][/base]/projects/{p}/branches/{b}) or theflexo://{p}/{b}shorthand for the configured endpoint, parsed byflexo.ParseBranchURL; the head commit is recorded in the sync state (-sync-state, or<output>.sync.jsonbeside-o). - Push a whole graph — landed.
sysml model.sysml -convert ttl -o <branch-url>replaces the branch's model graph through Layer 1'sPUT .../branches/{b}/graph(flexo.PutGraph, conditioned on the branch etagflexo.BranchETagreads; the harness's unconditional load stays asLoadTurtle), andflexo.Repository.Pushrefuses a moved head — one the sync state's last-seen commit does not name, or one that answers the write'sIf-Matchwith 412 — asStaleBranchErrorwith nothing written. The commit Layer 1 makes is recorded in<model>.sync.json; the token isflexo.EnvTokenas today. - What survives the hop. D2 and D1 decide how much of a pushed model the read path gets
back; the harness's figure is re-measured, not asserted, after each. Re-recorded with the
branch read and push in place: graph-load delivers 505 of 582 properties (59 of 59 elements
listed), the 77 undelivered all in the
sysx:extension namespace, and the element-wise apply now carries theFeatureValuememberships the standard vocabulary writes (17 of 17 elements read back on the initial commit).
Nothing here was a new subsystem; the order was D9.1 → D9.2, and D9.3 is the RDF track's existing order applied to this use.
A project under configuration management that consists entirely of views — view usages with
expose and filter over one or more imported projects, each bound by its own
@ProjectRef — owns no element worth editing: everything it shows is some other project's, and
with @ElementId on the materialized elements that ownership is explicit
(element-identity-annotations.md, nested scopes). Today the
sync tooling stops at the document: reposync.Diff produces one ChangeSet for one project
scope, GraphScope refuses a graph carrying two, and an edit made to an element as it appears in
the view project has nowhere to go but the view project's own branch, which is the wrong owner.
The rendered artefacts (-render, -doc) stay one-way; the round trip in question is
notation-to-repository by id, and it needs three things:
- Per-scope fan-out. A diff over a multi-scope document splits the local graph by enclosing
@ProjectRef, diffs each part against its own branch (Options.Baseper scope), and applies each as a commit to the owning project under its ownflexo.StaleBranchErrorguard. No partial success across scopes: a set that is not appliable in one scope refuses the run before the first write, asAppliabledoes today within one.-sync-staterecords one last-seen commit per scope. - Refusing the edit that would leave the view. If a write-back would take an element outside
the
filterof every view that exposed it — dropping the#systemRequirementtag on a requirement shown only byRequirementsView— the element vanishes from the project the edit was made in. That is the classical view-update problem, and the answer is a typed refusal in the change set (the same tier as the existing conflict verdict), overridable only by an explicit confirmation likeConfirmDeletes. The resolver's inherited view conditions (Resolver.inheritedViewConditions) already compute the exposing filters, so the check is a re-admission test of the edited element against them. - Variant-tagged edits under conditional configuration. A view project whose exposure
depends on a variant selection shows one variant's element at a time; an id says which
element, not under which selection it was shown, so a write-back must carry the selection
that was active and refuse when the target branch's selection differs. Elements with derived
(name-keyed) identity are excluded from write-through altogether: a rename through a view
would be a delete plus a create in the owner, which is the failure identity exists to prevent,
so the path requires
@ElementIdand offers-sync-mint-ids -sync-annotateto get there.
The gate is the live-stack harness (TestFlexoInterop) with a two-project fixture: a view-only
project over two owned projects, an edit through the view lands as one commit in each owner and
nothing in the view project, the filter-violating edit and the variant mismatch are refused before
any write, and a re-run finds nothing to change. Depends on D9.1 (reading a branch as notation,
which is how the view project materializes what it exposes). D9.1 and D9.2 are landed; independent
of D1/D2, since it moves whole elements by id and never inspects their vocabulary.
Before this item Convert wrote notation, text and Turtle, and read the same three. The OMG
SysML v2 API's own element form — JSON objects with @type, @id and the metamodel's properties
as keys — was read only as a measurement: flexo.Elements and flexo.ElementByID fetched what
the Flexo API serves after a Turtle load, so the harness could say what survived. Nothing produced
that form from a parsed model, and nothing parsed it into one. A framework that wants the
normalized abstract syntax without an RDF store in the middle — a tool exchanging elements over
the standard API, a client comparing two implementations element by element — had no format to
ask for.
What landed:
- The format.
api-json(aliasjson, extension.json) is aConvertformat in both directions, on every surface that names formats:sysml -convert/-from,%save model.json, the service'sConvertand the clients that name its formats (the Go and Python clients carry the constant, the rest pass the name through). It is the RDF mapping with a different serializer, not a second mapping:export.WriteAPIJSONwalks the graphToRDFbuilds — the metaclass as@type, the id as@id,sysml:properties as bare keys,sysx:ones with their prefix, IRIs as{"@id": …}spelled as the collection annotations spell them, the annotated collections (D3.4) as arrays, typed literals as JSON scalars — andexport.ReadAPIJSONisrdf_in.go's inverse over JSON, rebuilding the same triples and the same annotations, sosysml,ttlandapi-jsonconvert among themselves through one graph and the JSON of a model reads back to the Turtle of it triple for triple. What the mapping does not state is refused on both sides — an unannotated repeated property, a real JSON cannot spell, a document whose objects lack@idor@typeor use a prefix the mapping does not define — rather than dropped. Reference-valued properties are@idobjects, D7's question answered a second time; expression nodes are elements of the form with the ids theexpr:IRIs already had, and the reader tells them from elements by that id grammar. Documented in the RDF mapping, The API element form. - The ratchet.
TestCorpusAPIJSONRoundTrippins notation →api-json→ notation →api-jsonper file over every model underexamples/, sharing the Turtle gate's walk, verdicts and policy (rdf-corpus-roundtrip.md); CI runs both. The baseline is 354 of 356stable, the other twograph-diffbecause JSON respells a.1real as0.1— the one place the two forms' verdicts differ. Adjudicating it found and fixed two reader defects at their root: a name literal on a property the encoder never writes expression text on was read as expression text, and a real spelled.1was refused rather than respelled. - The live measurement.
TestFlexoInteropgained anapi-json-commitside that posts the emitted elements to the Flexo API's own commit endpoint asDataVersionpayloads, beside the Turtle graph-load and the hand-written JSON commit, and reports what each keeps. The service accepts the form wholesale: all 59 elements of the reference fixture are listed and readable by id, and the 505 properties it delivers are exactly the 505 the graph-load path delivers — no property survives one path and not the other. The 77 lost on both are thesysx:properties the service's reader ignores.
One reading is decided by the graph rather than the standard and is recorded as such: a collection of one member is written as an object, since the graph carries no multiplicity and the annotation that marks a collection is written from its second member; the standard API serves every multi-valued property as an array, and the reader accepts both.
KerML fixes the elementId of every named standard-library element as a name-based UUID
(RFC 4122 version 5): the library package's id is uuid5(NAMESPACE_URL, <prefix> + <escaped name>) with the prefix https://www.omg.org/spec/KerML/ or https://www.omg.org/spec/SysML/
for the KerML and SysML halves of the library, a named descendant's id is uuid5(<package id>, <qualified name>), and its owning membership's is the same with /owningMembership appended.
The pilot's XMI carries exactly these — ScalarValues::Real is
14c0aa22-5489-59b5-b438-ded26e83ba31 under ScalarValues's
40bb440c-5036-58e1-8675-5afccb8b8f1d — and so does every SysML v2 API server that serves the
library, so a reference to a library element agrees across tools without either side having
seen the other's model.
Before this item a library element's id was rdf.EncodeElementID over its qualified name
(ScalarValues__Real), the same derivation user elements get when no
@IdentityMetadata::ElementId declares one (the RDF mapping,
Element identity). Nothing was invalid — the encoded id is a legal IRI tail and the alphabet
Flexo's requireValidId accepts — but a graph, an API payload or an element-by-element comparison
that named Integer, kg or Performances::Performance named an element no other tool had,
and a project on Flexo that types its parts by the library's ids did not resolve against ours.
The encoded name is reversible (rdf.DecodeElementID recovers the qualified name exactly)
and a version-5 UUID is not, since it is a SHA-1; that is the trade, and the item keeps both: the
normative UUID is the library element's elementId and IRI tail, and sysml:qualifiedName —
which reading a graph back already takes the name from — stays the readable form.
What landed:
- The derivation.
internal/semantic/identity(normative.go) derives the element and owning membership UUIDs from a qualified name, with the pilot's quoting of names;identitymaps the bundled library's tiers to the two prefixes (kernel libraries to KerML, systems and domain libraries to SysML) and catalogs every named, non-aliased, non-shadowed library symbol once per library index.TestPilotLibraryXMIasserts the catalog against the pilot's ownsysml.library.xmiat the pinned release commit (scripts/download-pilot-library-xmi.sh, through the samescripts/pilot-pin.shthe corpora use): every id it derives is an element the XMI carries under the same owning membership, and every named XMI element is derived. The last to fall in line was the payload anaccepttrigger declares inside a transition (Actions::AcceptAction::aState::aTransition::apayload): the symbol index now holds it as a member of the transition, as the XMI does, so its qualified name — and the id derived from it — agree with the pilot's. The test keeps an exact list of pilot-only elements, empty now, so a divergence either way fails the gate. CI downloads the XMI and setsOPENSYSML_REQUIRE_PILOT_LIBRARY_XMI=1, so the test fails rather than skips there. Unnamed and implied library elements are out of scope by design — the norm gives them positional ids that depend on each implementation's implied-relationship closure, so they do not agree even between the pilot and other conforming tools. - The consumers. The RDF writer's IRI,
sysml:elementIdand owning-membership IRI take the normative id for a library element and the encoded name for everything else, and the reader does not re-materialize an@ElementIdfor a normative id; the Flexo sync classifies a normative id as neither declared nor mintable;identity.Inforeports which of the three sources an id came from (declared, normative, derived) and the language, and the LSP hover says so (Element id \14c0aa22-…` (normative, KerML)) while the minting code action stays off library elements. User elements are unchanged:@ElementId` when declared, the encoded name otherwise. - The ratchets. Neither
TestCorpusRoundTripnor the Flexo live-stack expectation moved: the corpora convert user models, whose references reach the library bysysml:qualifiedName, not by id, and the interop fixtures own no library element.
Extends D3's identity work; D11's api-json payloads are the first surface where a foreign
reader compares our library ids to its own.
-convert reads a SysML v1 model — OMG UML XMI 2.5.1 with the SysML profile, an Eclipse UML2
.uml file, a .mdzip archive — and writes it as v2 notation or Turtle with an
element-by-element report; see the migration reference and
guide chapter 11. The mapping covers structure, ports
and connectors, requirements, constraints, instances, allocations, user profiles, and behavior:
activities run as action defs and state machines as state defs under the chapter 6
debuggers, operations and receptions are the actions their owner performs, opaque JavaScript and
English bodies are translated within the subset the reference lists, and a tool's run
configurations migrate with the results it stored (-migration-results, -compare-results,
-clock-step). Over the TMT model five elements in six map or are approximated; the unmapped
rest is dominated by absolute and unparseable time events, call actions that call no behavior,
simulation verdicts stored in constraint slots, and views. It still ships experimental, and
lifting that is closing the items below and then measuring, not a documentation change. Each is
its own pull request:
- Units and quantity kinds.
«Unit»and«QuantityKind»instance specifications, and theunit/quantityKindtags of a«ValueType», are reported as unmapped and left as comments; the value type is written overScalarValues::Real. Map a value type whose unit resolves to a known SI or ISQ symbol ontoISQquantity values withSIunits (attribute def Mass :> ISQ::MassValue, defaults as1200 [SI::kg]), and keep the comment for a unit that does not resolve. L5's unit canonicalization supplies the symbol table. - The report and the results over gRPC and in the clients. The report and the results
sidecar exist only where the CLI writes them;
Convertanswers the migrated content, the canonical format names and the experimental notice, nothing more. Add the report toConvertResponseas a repeated entry message with the fields of the JSON form (id, kind, name, verdict, target, note), populated only when the source is v1, and surface it as aConversionattribute in the Python and Go clients, so a program can gate on verdicts as a script gates on the.jsonreport. Node, Java and Rust read the field from the generated code without a wrapper, as they do the notice. A Track I change to the wire contract in the same pull request. - Stable identity for a re-migration. The v1
xmi:idis kept only in the report; the notation carries no ids, so migrating a model twice produces two unrelated v2 models, and an edit made in v2 cannot be matched against a later v1 export. With element identity annotations, write thexmi:idas the element's identity, so a re-migration can be diffed against the edited model rather than replacing it. Gated on D3's identity work. - Stable, then measured. With 1–3 landed, re-run the TMT measurement the reference quotes
and pin the per-verdict counts as the corpus ratchet pins round trips; the mapping's forms
become subject to the compatibility path that lifting the experimental notice promises, and
the notice is removed from every surface at once (
convert.IsExperimental, the Python warning, the GoConversion).
Independent of D1, D2 and D7, which concern the RDF form the migrated model may be written into, not the migration.
At v0.6.0, internal/exec/runtime/testdata/conformance/known_failures.txt named three cases
that TestExecutionConformance skipped rather than ran, each a fixture whose expectation is
derived from the Kernel Semantic Library in
behavior-semantic-oracle.md ("What the executor gets wrong") and
not met by internal/exec/runtime/action_executor.go. All three landed (#116, #120) and the
file now lists no case — its one remaining line of prose reads "The list is empty: every derived
case passes" — so the gate runs all 770 cases and skips none, and the three rows in
spec-compliance.md moved from known failure to faithful. The track is closed; the items
below record what each fix did and what it deliberately left as it was.
Fixture action_node_with_two_incoming_successions_runs_once (hits = 1). Landed with F2 in
#116: a plain action node two or more successions reach fires when every incoming succession has
delivered one token, the arrivals collapsing into the one token that performs it. A plain node in
a loop or behind a decision still re-performs once per pass — it awaits a succession only while
some token can still reach its source — so nothing that ran once per token by design now waits.
Fixture action_join_one_token_per_incoming_succession (log = 12). Landed in #116: a token
records the succession it travelled (Token.Via, the lowered ActionEdge), a join fires when
each incoming succession has delivered one token, a second token over an already-delivered
succession waits for the next firing instead of standing in for another, and a join one of whose
successions no token can travel deadlocks (ErrActionDeadlock) rather than firing on a count.
The REPL's %tokens says which succession a held token arrived over and which it awaits. #119
then made a breakpoint on such a node pause once, before its one performance, and made a step
move each token at most once, so traces of forks and joins gained a step boundary between the
last arrival and the node's performance — the same statements in the same order.
Fixture action_merge_loop_reenters (level = 100, passes = 3). Landed in #120: a merge
is one MergePerformance per arrival, as Actions::MergeAction declares, so a loop re-enters it
as often as its guard sends the token back; a fork whose branches both reach a merge yields one
downstream token per branch (collapsing them is a join's job, and a merge is the one
multi-incoming node F2's synchronization does not wait at); and a merge's body runs before the
guard on its outgoing succession is read, so a write in the body decides its own guard. A loop
with no exit still ends on the action step budget (ErrActionStepLimitExceeded).
The library states a partial order between performances and fixes some outcomes; it does not
order two steps no chain of HappensBefore connects, and it gives no conflict rule when two such
steps write one feature (behavior-semantic-oracle.md, "What the
library fixes, and what a trace adds"). At v0.6.0 the executor picked one linearization —
tokens stepped in descending index order within a step, a fork's branch declared last stepped
first — and every fixture pinned that one: action_fork_branches_write_one_feature recorded
x = 1 as the executor's scheduling, not as a derived value, and its compliance row was
approximate for exactly that reason. A conformance case could not say "1 or 2", a trace could
not say where the executor chose, and nothing checked that the linearizations it did not take
would also have met the derivation. This track made the choice explicit and checkable. All four items landed
(#110, #123, #125, #134), with #141 and #138 as follow-ups; the fork case now admits both x = 1
and x = 2, 19 cases list outcomes, 5 carry a .trace.order, and the harness explores every
case that lists outcomes. What each leaves is stated under its item.
It was a prerequisite to Track E: E1–E7 edit the same executor loops
(stepActionExecutionNode, stepJoinNode, stepMergeNode, the fork and the token list) that
S1–S4 instrumented and parameterized, and a termination, interrupt or streaming semantics added
on top of an implicit scheduling order would have had to be re-derived once that order became a
policy. Track F's three fixes were the same loops again and came first, because a policy over a
join that miscounts is a policy over a defect. Both are in; E waits only on the release.
The unit these items are stated in is the choice point (#123): a pick among alternatives the
library leaves unordered — several steppable tokens in one action step, several holding guards
at a decision, several enabled transitions out of one state for one event or change, several
tokens writing one feature in one step, and since #136 and #141 the order several executors due
at one instant run in (due order) and the order orthogonal regions react to one event in. Each
is a choice trace line naming the alternatives and the one taken, an informational
choice-point diagnostic on ExecuteAction, ExecuteState and RunAnalysis, and a count after
%step, %continue and %advance. A guard that cannot be evaluated in the preview is not an
alternative and not an error but a guard-unevaluable diagnostic; the innermost-transition-wins
rule between a substate and its enclosing state is spec-defined order and is not reported.
Landed in #110. Where the library leaves more than one result open, .expected.json lists
every admissible result under outcomes and must cite, in admissible, the section of the
behavior semantic oracle deriving them — the run must match exactly one, and a missing or
unresolvable citation fails the case — and a <case>.trace.order file of a < b lines states
the partial order the recorded trace must satisfy, beside or instead of an exact golden. A case
with neither keeps its meaning; the default schedule and every exact golden were unchanged. What
it leaves: outcomes is a set of complete results, not a per-feature range, and a case that
lists it is graded by S4's exploration, not by one run.
Landed in #123 as the choice point described above: each step where the library left a pick
open is recorded with the alternatives and the one taken, in the trace, on the wire and in the
REPL summary, while what the executor does — reverse token order, first holding guard, first
declared transition — is unchanged, so every existing result and trace is the same. The guards
and transitions after the first that holds are read in a preview that is undone. #141 added the
one kind the fixed policies had been silent about, the order orthogonal regions react to one
event in (choice on <trigger>: states <a>, <b> react), reported under every policy.
Landed in #125. The policies are reverse (the default: reverse token order, first holding
guard, first enabled transition — every existing result and trace unchanged), declared (tokens
in spawn order, guards and transitions in declaration order) and seed:<n> (a pseudo-random order
the seed fixes, so one seed replays one run on every platform). One spelling everywhere: sysml -schedule <policy> for -action, -state and -analysis (-calc has no choice to make),
%schedule [<policy>] in the REPL (applied to the runs started after it; a debugging session
under way keeps its own), a schedule field on the three execution requests advertised as the
schedule capability and taken as an option by the Go and Python clients, and a schedule pin
on a conformance case. A spelling naming no policy is refused before anything runs
(INVALID_ARGUMENT on the wire). The conformance suite also runs whole under declared and
seed:1, requiring every case that pins no policy and lists no outcomes to reproduce its
default outputs; that is where the 34 per-policy trace goldens come from. What it leaves: a send
to a same-named port pins reverse until the via-less accept that over-matches it is fixed, and
until #141 seed:<n> could not vary the order two regions react to one event in — it now draws
and replays it, while reverse and declared still take declaration order. A5's clock landed on
top of this as the due order choice point (#136), as this item said it would.
Landed in #134. sysml -schedule explore[:runs=N,depth=D] runs a behavior once, then
replays it from the start on a fresh executor of the same loaded model, following the recorded
prefix and taking the next untried alternative at the frontier, the first run's choices varied
earliest first, until every choice sequence is spent or a budget is hit (1024 runs and 64 choice points per run by default; hitting
either is exit status 2, never a silent truncation). Runs agreeing on the observables the
harness compares are one outcome; a run that fails under some order is an outcome of its own. The
responses gain repeated outcomes and an exploration status, advertised as schedule_explore;
the Go and Python clients gain Explore*/explore_* calls and the Node, Java and Rust clients
the capability name. The harness runs every conformance case that lists outcomes under
explore and fails when a listed outcome is unreachable or an unlisted one is reached, so the
list is exact; exploring the whole suite found one case pinning a scheduling artefact (two accepts
on one port addressed by two sends), restated as the two outcomes the oracle derives. What it
leaves, by design: the REPL refuses %schedule explore with a typed error naming the CLI and the
wire, since %action and %state step one run (confirmed against bin/sysml at this baseline);
cases without outcomes are not explored; and the budget is the author's to raise
("exploreBudget": {"runs": N, "depth": D}), not the harness's to sample past. #138 wrote it
up: every open ordering in the oracle names the outcomes or .trace.order that encodes it and
the run and outcome counts explore reaches, and the behavior guide has a section on models with
more than one valid run. As landed, explore permuted the tokens of one lockstep step — every
steppable token moved once per step — so a branch of two nodes could never both run before a
concurrent branch's one, and a fork of left1 { x := 1 } → left2 { y := x } against
right { x := 2 } explored complete with two outcomes, missing x = 2, y = 1. An action step
under explore is now one token advancing one node, the tokens able to act are picked among
afresh after each move, and complete covers every interleaving at body granularity
(action_explore_write_between_branch_nodes pins the three); the fixed policies keep their
sweep, so no default trace moved, and the oracle's run counts were re-derived at the new
granularity (action_merge_fork_branch_and_loop now needs explore:runs=10000 to complete).
The track's last mile: the open picks carry the odds
a model states. Stochastic::Probability weights a transition the way it already weighted a
decision's succession — one more choice-point kind drawn by weight under seed:<n>, enumerated
by explore, followed by replay: — among the transitions otherwise equally eligible for one
dispatch, so innermost-wins still resolves before any weighting. And where the table once
stopped at the set, explore now reports each outcome's probability — a linearization carrying
the product of its picks' shares, a weighted pick's stated weight's share and an unweighted
choice's uniform 1/n, ≥-marked while the search is incomplete — and check the probability
mass of the paths reaching each violation (mass beside massLowerBound in the JSON report,
Outcome.probability / ExplorationStatus.probabilities_lower_bound on the wire, and an
optional probability per outcomes entry in the conformance schema, exercised by the weighted
transition fixture).
The runtime executes actions, state machines, calculations and constraints against the lowered
IR (internal/ir/lower ActionGraph/StateGraph, internal/exec/runtime). The behavior-execution
items were once listed as unsupported or approximate; on this tree E1, E2, E4, E6, E7, E8, E9
and E10 are landed, while E3 and E5 are closed by design record, as is the compliance mapping's
last UML-referenced action item, exception handlers. The landed items have conformance
fixtures under internal/exec/runtime/testdata/conformance/, trace goldens, and robustness coverage;
the design closures are recorded in
expansion-regions.md, protocol-state-machines.md
and exception-handlers.md.
The track records how each item was decided against the SysML v2 notation, the Kernel Semantic
Library, the Systems Library and the available execution evidence.
The UML comparison remains useful for explaining the closures: SysML v2 has no expansion-region,
protocol-state-machine or exception-handler notation, while its actions and states provide the
corresponding for, flow, acceptance, terminate and exhibited-state-machine forms. The proof
for landed execution items follows the four-layer contract in AGENTS.md §5.2, with the pinned
pilot used for expression-level adjudication rather than action or state execution.
Landed ahead of E1–E7, all merged: the one structural finding of the review
— nested action nodes shared the enclosing action's flat feature space, so p.v and q.v
collided and same-named outputs overwrote each other — is
PR #823, which gives each performance its own
frame with the callee's pins, precedence-ordered binding (flow payload, then bind, then the
declared value) and block-flow nodes inside if/loop bodies as real nodes. Message delivery
through a binding connector between an assembly's boundary port and a part's port
(PR #839) fixes accept … via i never firing
on the inner part. On the debugging surfaces, PR #843
makes every expression surface read the instantiated object after -instantiate (the stale 0
beside %features' 1, and %eval in #1 being an unresolved reference),
PR #805 reads a valueless feature as <unset>
in %eval in and lists behaviors, PR #808 adds
%send and PR #809 one object-reference grammar
for every REPL command, reconciled against #810. What the review found after #810 was that
%state <machine> in its one-argument form attached a fresh, detached performance rather than the
one materialized object that exhibits the machine (after %instantiate TA::Sys; %state lp; %advance 2.5 [s], object #1 still read n = 1 where %state #1 gave n = 3). The cause was a
StateExecutor built with no self, so entry/do writes landed in the executor's own frame;
PR #845 makes the machine form walk the held
objects and attach to the single exhibitor's running machine, refuses with a typed error naming the
objects (or, before any object exists, the types) when there are zero or several, and leaves a
truly unbound state def running detached; re-run at this baseline the same probe reads n = 4
on object #1 after %advance 2.5 [s] (entry, then the timed re-entries at 1 and 2). The double
initial-do the review first reported is not reproducible on main since #810 and is retired.
Since then: %state <machine> on a state machine with a typed body attaches the same way (#856);
the state executor refuses the trigger arguments validation refuses (accept after 5 without a
unit, a conditional when) instead of running them (#833, #871); send arguments are validated
and send new Def(args) constructs the message it sends (#838, #875); and a message through a
binding connector at a boundary port routes in both directions (#839).
Since the tag, on develop, the state-executor fixes adjudicated in the PSSM referee
(pssm-referee.md) moved its baseline from 36 pass / 23 fail /
39 not-expressible / 3 terminate-gap / 2 differs-by-design at the tag to 56 pass /
13 fail / 33 not-expressible / 1 differs-by-design of the suite's 103 tests, pinned in
docs/project/pssm-referee-baseline.json. The first eight moved it to 45 pass, the 3
terminate-gap tests (E1's) and 2 differs-by-design unchanged: a fork may enter orthogonal
regions that have no initial state (#297); a transition from a substate into its enclosing
composite does not re-enter it (#311); several completion transitions out of one state are one
choice point (#313); a junction with several enabled branches draws one as a choice point
(#318); a join runs the effect of every incoming segment, their order a region-order choice
(#317); a transition into a history pseudostate restores the configuration it is leaving (#295);
the referee's translation carries the values a test's constructor writes (#314) and refuses a
guard whose behavior acts on the model rather than dropping the call (#315); a join's segments
fire with their own trigger bound, and a refused join is undone whole (#322). E1 then landed:
the referee translates the terminate pseudostate, its terminate-gap bucket is retired, and the
baseline was 46 pass / 17 fail / 38 not-expressible / 2 differs-by-design — Terminate
003 passes, Terminate 001 and 002 failed on the region-entry order the open finding then
covered. The region-order choice points closed that finding (#384, then #438 and #513): the
order orthogonal regions are entered, exited and branch in, and a due do step against the
dispatch due at the same instant, are recorded choice points the fixed policies resolve one way
and check, replay and explore enumerate, which moved the baseline to 51 pass / 13 fail
/ 38 not-expressible / 1 differs-by-design and Terminate 001 into pass; the referee's
own translation then moved five not-expressible rows into pass — the driver performs the
tester's steps in the tester's order, a synchronous call returns the operation's outputs and a
standalone state machine reads as the target class (#486); an entry, do or effect behavior's
parameters bind to the triggering event's data and a behavior producing the operation's result
is an action def with out parameters (#490) — giving the 56 / 13 / 33 / 1 above. The
do-step draw is at the grain of one token move of the do flow (#513) but is drawn only once a
region-entry move has settled, so a due do step is not yet drawn against a sibling region's
remaining entry units — the one reason Terminate 002 keeps, which #525 (open against
develop) addresses (region-order-scheduling.md).
Every remaining fail is attributed in pssm-referee.md: five to a differs,
v2 silent row of the alignment note; seven to a finding of the record — Terminate 002's one
missing trace, the
runtime's last open gap there; Entering 010, Entering 011 and Junction 005 a translation
limit; Transition 017, History 001-C and History 002-B defects of the suite recorded in
omg-issues.md — and Exiting 002 to a defect of the suite alone. The
object-model item E7 waits on also landed: a run creates
objects by new T(…) and destroys them by destroy, a context holds several objects of one
usage, and a destroyed object is released from all T with the behaviors it performed
terminated (below, Dynamic object creation and destruction).
Landed in two change sets, the action half and then the state and occurrence half (SM38 in
docs/internals/design/precise-semantics-alignment.md). The parser had accepted a terminate
action usage in every position the grammar allows and lowering carried it losslessly; the runtime
refused every one with 'terminate' in a body is not executable. That refusal is gone from
runtime/action_statements.go and runtime/state_statements.go, and lower.Effect names the
terminated occurrence rather than leaving the executors to re-derive it: Terminates classifies
the target (TerminateContaining for a bare terminate;, TerminateEnclosing for a terminate
action usage then stop; reaches, TerminateNode for a name that is an action node of a flow
around the statement, TerminateOccurrence for this or a feature chain) and Target/
TargetExpr carry the expression, in the scope it is written in.
Actions (runtime/action_terminate.go): then terminate; ends the performance whose flow it
is a node of with the outputs assigned so far, dropping every other token of that performance —
a forked sibling still running, one parked at an accept — lowest token ID first, which the
trace records (terminate <performance>: dropped tokens …); terminate; in a nested node's body
ends that node and the parent goes on along its succession; terminate <node>; ends every
ongoing performance of the named node in the flow around it, earliest begun first, the one
running the statement included, and a performance whose step a parked token had yet to begin is
begun and ended in the same activation (beginPending/endPending, traced as ended before it began or ended waiting) so the result does not depend on which fork branch the scheduler
stepped first. Occurrences (runtime/occurrence_terminate.go terminateOccurrence):
terminate this; in a part's behavior and terminate <feature chain>; evaluate the target and
end the occurrence's lifetime, with its owned parts, the behaviors it exhibits or performs and
every action or state executor running on it (Context.endOccurrence, endBehaviorsWith; an
executor whose performer ended between two of its runs ends as terminated at its next,
performerEnded). States (runtime/state_statements.go, state_route.go terminateAt,
state_executor.go terminateMachine): a terminate; in an entry, do or exit body ends
that behavior at the statement, the state stays active and dispatch goes on — and since #466 and
#479 a braced entry { … }, do { … }, exit { … } or transition do { … } block is the one
anonymous action usage SysML.xtext reads it as (the same tree as entry action { … }), so the
terminate; ends the whole block rather than the one statement it was written in, a declaration
inside the block is local to it, a do block still runs one statement a round, and the RDF
mapping writes the block as that nested ActionUsage (a Turtle graph in the older
statement-by-statement shape is refused as unsupported); a transition whose
target is a terminate action usage (accept Abort then stop; action stop terminate;, §7.18.3)
exits its source and runs its effect, then ends the machine's performance with no further exit,
the running do behaviors abandoned (abandonMachine) and no state active — reached directly or
through a choice, junction or join, from a composite state or one region of an orthogonal one.
The outcome is Outcome.Terminated with an empty FinalState, the executor's state
StateTerminated (distinct from StateCompleted), the REPL's State machine terminated and
Execution state: Terminated, the LSP debug snapshot's terminated. A terminate naming a
performance that ended, a value that is no occurrence, or an occurrence destroy emptied is a
typed error (ErrPerformanceEnded, ErrTerminateOccurrence, ErrTerminateTarget,
robustness_terminate_test.go); a calculation still refuses it as ErrCalcSideEffect
(robustness_test.go:calc_terminate_is_rejected). The PSSM referee translates the suite's
terminate pseudostate to the same spelling, which retired its terminate-gap bucket
(docs/project/pssm-referee.md).
What it leaves. A fork branch of a state machine cannot target a terminate action: the
lowering's fork rule takes states as branch targets (lower/fork_plan.go), and the spelling
reported rather than routed. The training corpus's 19. Terminate Actions/Terminate Actions Example-1.sysml (MonitoredActivity) parses and every terminate it uses runs, but the model
still stops at initialize: its nested node performCriticalActivity is two performs with no
first and no succession between them, so the flow-start rule (lower/case_body.go
CaseFlowStart, runtime/action_subflow.go noFlowStart) reports two possible starts — no succession leads to "monitorCriticalActivity" or to "criticalActivity" — where §7.17.2 lets
unsequenced nested actions start concurrently when their container does. That is a flow-start
item, not a terminate one, and it is what the example waits on.
Landed. An inline do body pauses after each statement, so a transition leaving the state
after s1 leaves later statements such as s2 and s3 unrun. The body remains resumable through
loops, nested blocks and branches, and its trace and conformance fixtures cover the interruption.
Fixed scheduling policies finish a do round before they dispatch; under check, replay and
explore the dispatch is drawn against each token move of the body, so the round-first run is
one of the interleavings enumerated.
Before it landed. SysML v2 has no interruptible-region notation; the corresponding state
semantics interrupt a still-running do action when a transition leaves the state. The runtime
formerly advanced an inline body as one action and interrupted only between rounds, so the work,
target and proof were to add statement-boundary resumption and drop pending statements on exit.
Closed by its design record, expansion-regions.md: the iterative
form is for; the parallel form is not SysML v2. SysML v2 has no expansion-region notation.
Its iterative half is the for loop (§7.17.12, Actions::ForLoopAction), which the runtime
executes in every body position (runtime/action_statements.go, runtime/statements.go
forLoop), one iteration after the other, with the step budget bounding it — sequential by
definition, ForLoopAction walking seq by an index. Its parallel half — the body performed
once per element of a collection, all performances ongoing at once — has no standard spelling.
The record adjudicates the candidate the item asked about, a multiplicity on a performed action
usage with a flow delivering the collection to its input, against the pinned specifications,
the bundled library, the four OMG corpora and the pilot, and finds every source silent on it:
§7.17.2 gives a usage's multiplicity no meaning beyond a count of performances, which KerML
§7.4.7 and Annex A.3.6 fix by the connectors attached to the step and never by a value's
elements; a Transfer delivers its whole payload to one target and partitions nothing;
Performances.kerml's performances/subperformances are [0..*] and unordered; the corpora
write [*] for "any number of times over a lifetime" (perform action takePicture[*]) and for
for "once per element"; the pinned pilot performs no actions. Adopting the reading would be an
extension with nothing to adjudicate its rules against, so the runtime stays as it is: one
performance per token, a fork duplicating control and not a collection, and a collection
delivered to a one-valued pin the ErrMultiplicityViolation it is.
No executor work follows. The per-element performance, join on completion, ordering and
streaming interaction the item listed if a spelling exists are not designed, and the proof
fixtures it named (three elements performed concurrently with outputs collected, the interleaving
golden, the empty collection, a body failing on one element) are not written, since there is no
rule for them to prove. A model that needs per-element concurrency today writes the elements as
distinct nodes under a fork, or accepts the sequential for. Should someone need more — a
per-element behavior whose for result is wrong or too slow — the record's option (a) is the
starting point, as an explicitly non-standard extension the compliance mapping would have to flag,
or a later specification revision re-adjudicates the record. No other item depends on this one.
Landed. A flow between two action parameters is a streaming flow, and only the
succession flow spelling moves its value when the source completes. SysML v2 §7.16: "the input
and output parameters are streaming unless designated as succession flows" — a streaming flow
"can be ongoing while both the source and target action are being performed", while a
succession flow "cannot begin until the source completes" (Flows::Flow :> Message, FlowTransfer; Flows::SuccessionFlow :> Flow, FlowTransferBefore). The parser had kept the
distinction (ast.Usage.IsSuccessionFlow) and the lowering lost it: lower.ObjectFlow now
carries it as Kind (FlowStreaming, the default, or FlowSuccession), lowerFlow sets it and
succeedFlow reads it for the succession edge a succession flow also states, so the runtime
re-derives nothing from the declaration.
Streaming (runtime/action_frame.go streamFrom, streamFlow): each write to the source
pin — an assignment in the source's body, a nested node's output carried back into it — is
carried at once along the streaming flows out of the node to the pin of every ongoing
performance of the target, found by E1's ongoing; a target that reads its pin between two
writes sees each. A value written while no performance of the target is under way waits at the
target's pin as a flow's value always has (pending), and a further write from the same source
performance replaces it (stage, actionFrame.staged), so a target that begins after the
source reads the pin as the source left it; the writes of distinct source performances wait one
per target performance, oldest first, so a stream inside a loop body — the value of one pass
taken by that pass's target — and fork/join around the two nodes all read what was written for them.
The source completing carries nothing more for a pin it streamed (actionFrame.streamed), so
no value arrives twice; a source that completes with the pin never written is ErrFlowSource.
A write after the target's last performance ended reaches no performance: it waits, the target's
next performance takes it (takeDeliveries), and the enclosing performance completing with it
still waiting is ErrStreamUnreceived (actionFrame.unreceived, checkStreamsReceived); a
stream to a pin the ongoing target does not declare is ErrNodePin at the write. Succession
(runtime/action_executor.go applyDataFlows → deliverFlow): unchanged — the value the pin
holds at completion moves, and the target begins after the source.
Proof. lower/action_flow_kind_test.go (the kind follows the spelling; only the succession
kind orders); conformance action_flow_streaming_producer_consumer (a producer loop writing 1, 2,
3 beside a consumer loop reading each: total 6) with its trace golden showing the writes and
reads interleaved, action_flow_succession_producer_consumer (the same nodes in sequence: the
consumer reads 3 three times, total 9) with its trace golden, action_flow_streaming_before_target_begins,
action_flow_streaming_in_loop_body; robustness_streaming_flow_test.go (a source that never
writes, a target over before its source wrote, a stream to an undeclared pin). The
spec-compliance.md Actions map's object-flow row is split by kind and the compliance row records the two flow kinds.
What it leaves. A source whose body writes its pin several times before any target performance is under way leaves the target the pin's value — its latest write — as a target performing beside each write would read; only a target performance under way sees every write. E3's parallel form, when it has a spelling, decides the streaming consumer of its elements with it.
Closed by its design record: protocol state machines are not a SysML v2 construct; reception
ordering is an ordinary exhibited state machine. The record establishes that the legal order of
receptions on a port or part is expressed with accept … via <port> and runs on the exhibiting
object. Ordering operation calls, post-conditions, ProtocolConformance and static sequence
checking have no SysML v2 spelling and are not OpenSysML gaps.
What it leaves. The order the machine declares is enforced only for events a debugger injects
directly (StateExecutor.SendSignal → dispatched, dropped, reported in AdvanceReport.Dropped;
the REPL's %send refuses one by machine and state). A message a model sends that the active
state neither accepts nor defers is not dropped: it waits on the context-wide bus and is taken by
the first later state that accepts it, so an out-of-order Read before Open is counted as if it
had come after (the record's second probe: reads = 2, nothing reported). A machine exhibited by a
port definition runs and answers the debugger's messages to the port object, but does not
take a model's messages routed to that port.
Optional follow-up (not a spec gap). A future opt-in policy may make a refused model arrival a typed error and align port-definition routing with the direct-injection path.
Landed. Context.InvokeOperationWith(inst, name, OperationArguments{Positional, Named})
(runtime/invoke_operation.go) takes the ordered list; InvokeOperation keeps the named map and
delegates to it. A positional list binds to the operation's effective input parameters —
semantics.Model.SignatureParametersOf, the in/inout parameters signatureOf gives an
invocation expression in signature order, out and result excluded — so a trailing defaulted
parameter may be omitted, an inout parameter takes a position and comes back as a result, and an
out parameter takes none. Among same-named members, operationOf selects through
semantics.Model.SelectAmongArguments, the overload selection the expression evaluator uses, in
its PerformsOperation mode — every behavior admitted alike, an expression's preference for a
calc set aside — so two calcs of one name are told apart by arity, an action and a calc of one
name by the arguments' types, and a list none takes is refused. A surplus is
ErrOperationArity (operation … takes N input parameter(s), got M argument(s)), a list mixing
the two forms is ErrMixedArguments, and a required parameter left unbound is still
ErrUnboundParameter. The REPL's %invoke <object> <op> takes bare expressions or <p>=<expr>
pairs (repl/meta.go parseInvokeArguments), refusing a mixed list and a parameter named twice
before the object is reached. Proof: runtime/classifier_behavior_test.go
TestInvokeOperationWithPositionalArguments, runtime/robustness_positional_invoke_test.go,
repl/classifier_behavior_test.go TestInvokeBindsPositionalArguments and
TestInvokeReportsItsFailureModes; the bullet left the compliance list. The gRPC surface exposes
no operation invocation, so nothing there changed.
Before it landed. Context.InvokeOperation(inst, name, args map[string]Value)
(runtime/invoke_operation.go) runs a member of an object's type with the object as performer,
whichever behavior the member is — an action through ExecuteActionPerformedBy, a calc through
the calc invocation with the object as its featuring object, a constraint through condition
evaluation — and TestInvokeOperationPerformedByTheObject (runtime/classifier_behavior_test.go)
covers all three. The compliance bullet used to name an operation "given as a calc or
constraint" beside the positional form; that half is closed by the Classifier Behaviors row that
says so, the bullet is re-worded to the positional form with this record, and this item is that
form only. Arguments bind by name and only by name: operationInputs takes a map, binds each
in/inout parameter by its name and refuses a missing one (ErrUnboundParameter: parameter … has no argument and no default) and an unknown one (ErrUnboundParameter: … is no input parameter of operation …). The REPL's %invoke <object> <op> [<p>=<expr>] (repl/meta.go
operationArguments) formerly required named arguments, while model invocation expressions already
supported positional binding. The target was to expose the same positional-or-named choice through
the API and REPL, never mixing the two forms; the implementation and proof then added the ordered
API list, bare REPL expressions, mixed-list and surplus-argument diagnostics.
Landed in one change set. Before it an object was materialized once, from its declaration,
and nothing destroyed it: new T(…) built a message-shaped value for send, an object written
into a feature was classified by it but owned by nothing, destroy refused an object whose state
machine was under way (ErrOccurrenceLifetime), and a destroyed object stayed in all T. Now an
object created while a behavior runs is a first-class occurrence of the run
(runtime/signal.go evalConstructor → EvalContext.constructObject): it has an identity of its own,
begins its life where it is made, is classified by the type it is created as, and starts the
behaviors its type exhibits or performs as an object materialized from a declaration does. The
spellings are the specifications': KerML §7.4.9's instantiation expression new T(args), in any
expression position — an assignment, a feature value, an argument, send new Data(…) — and the
library's create/addNew/addNewAt; SysML v2 defines no other textual constructor for an
occurrence of a definition, and the compliance record says so. Writing an object into a feature
holds it (runtime/classify.go holdWritten, from SetFeatureValue): the feature's type
classifies it and, where the feature is composite and the object owns no whole yet, the object
becomes a portion of the owner, so assign cars := (cars, new Car(n)) in a loop leaves the fleet
holding one car per iteration, each a distinct object all Car, feature chains, %features,
isDuring/istype and routing reach. destroy (runtime/lifetimes.go) ends the occurrence and
its portions, terminates the state machine it exhibits and the actions it performs where they
stand (occurrence_terminate.go endBehaviorsWith), and releases it from the extent
(extent.go objectsOf); a feature still naming it keeps the value and reading through it is
ErrOccurrenceDestroyed, the library declaring destroy over the occurrence and not over what
refers to it. Under explore creation and destruction are ordinary moves — identities are
allotted in run order, so every linearization of two creating branches reaches one outcome and a
destroy racing a read reaches exactly two — and a snapshot carries created objects, a destroyed
one with its behaviors terminated. Proof: conformance object_created_by_constructor and
object_destroyed_at_runtime with trace goldens, robustness_object_lifecycle_test.go
TestRuntimeRobustnessObjectLifecycle, TestDestroyEndsTheMachinePerformed; the compliance
record's Dynamic object creation row and its create/destroy row, and the "not supported"
bullet gone.
What it leaves. The RDF mapping is of the model and exports no run's objects. E7, the send side, has landed (below).
Landed. lower.Send carries the to expression (TargetExpr, ReceiverExpr) beside the
name it reduces to; a target that is neither a name nor a feature chain — cars#(2),
garage.cars#(2), new Car() — is evaluated in the sending performance
(runtime/signal.go valuedTargetAddresses, receiverObjects) and the message is addressed to
the identity of every live object it yields, while a plain name still resolves by usage as before.
In the via form a to that names no receiving node of the sender is evaluated the same way and
the routed copies are kept only where they reach one of those objects (routedReceiverObjects).
A receiver yielding no object or a data value is ErrSendTargetNotObject, a destroyed one
ErrOccurrenceDestroyed, a routed receiver no connection reaches ErrUnreachableSendReceiver.
Proof: conformance send_to_second_object_of_usage and send_via_to_second_object_of_usage with
trace goldens, send_to_object_held_in_feature, send_to_object_through_chain,
signal_test.go TestSendToConstructedObjectReachesIt,
robustness_send_to_object_identity_test.go TestRuntimeRobustnessSendToObjectIdentity; the
compliance record's send rows updated, the Known Limitations bullet and the "not supported" bullet
gone.
Before it landed. A send formerly resolved a named usage to the one occurrence held by the context, so a second object of that usage was unreachable through the usage name. The target, work and proof were to evaluate the receiver by value, address the resulting identity, and pin the second object's acceptance with conformance and trace coverage.
Landed. Effective values and scopes are lowered into StateGraph; the state executor applies
held-entry boundaries, exposes the entry step choice point, and carries it through checking,
exploration, replay and snapshots. Conformance fixtures cover state, scoped and machine
redefinitions, with default twins preserving the default path; the two remaining invalid cases
are typed refusals for a missing occurrence scope and a scope that is not an ancestor of the
redefining state.
Known limitations. Fork/shared-path enterLazily entries are not split. The boundary applies
only to entry cascades; exit and effect sequences are unchanged. A parallel region owner stays
active during an intra-region transition, so its entry behavior does not re-run.
Before it landed. The library defaults held all entry transitions to the end of the machine
entry cascade, and redefinitions of isRunToCompletion or runToCompletionScope were rejected.
The target was to lower effective values and ancestor scopes into StateGraph, expose free versus
held dispatch as an entry choice point, and retain typed refusals for missing or non-ancestor
scopes. The proof covered default twins, state/scoped/machine redefinitions, invalid scopes, and
the resulting exploration and replay behavior.
Landed with the state-machine rules of docs/internals/design/precise-semantics-alignment.md
(SM11). Before, a then done; in a composite state's body ended the whole machine
(completeIfDone → machineComplete) and a nil-trigger transition out of that composite was
never scheduled, so state outer { … then done; } transition first outer then next; never
reached next. States.sysml binds done to the StatePerformance::endShot of the state whose
body names it, and TransitionPerformances.kerml places a transition's effect and target after
its source's performance, so the machine's end there had no basis in the library. Now done in a
composite's body lowers to that composite's own completion vertex
(lower/state_graph.go:completionOwner); once its do behavior and every region have ended the
composite completes and scheduleCompletedComposites → scheduleCompletionTransitions queues its
nil-trigger transitions as completion events at the current instant, ordered as a leaf's are; and
machineComplete ends the machine only when its top-level regions are all at done. A completed
composite with no enabled completion transition stays completed and active — both PSSM
(§8.5.9, the completion event is lost) and SysML v2 §7.18.3 ("does not necessarily terminate
immediately") agree that nothing ends there, and neither says more. Pinned by
state_outer_completion_to_next, state_composite_completion_then_machine_done,
state_composite_completion_nested, state_composite_completion_inside_region,
state_completion_nested_regions and state_entry_transition_nested_done (each rewritten with a
completion transition out of the composite), their _stay_active siblings, and the unit tests in
state_completion_test.go; spec-compliance.md's completion rows carry the rule.
Landed with the same change set (SM30). pseudostates.md had always described a choice as a
dynamic branch whose guards are read when it is entered, while resolveRoute picked the branch
for choice and junction alike before the incoming transition's effect ran, and a code comment
called the two indistinguishable for a guard over state data. Now a route is settled before firing
only up to the first choice — junctions along it statically, as before, a junction with no
enabled branch still meaning the transition is not enabled — and firing exits the states every
branch of the choice leaves, runs the effects into it and only then reads its guards
(state_route.go:travel → resolveChoice); several enabled is the existing ChoiceTransition
point at choice <name>, enumerated by explore; none enabled is the typed
ErrChoiceWithoutBranch. On a chain each pseudostate follows its own rule at the point the route
reaches it. Pinned by state_choice_after_incoming_effect (assign x := 1 then pick; … if x == 1 then seen reaches seen), state_choice_dynamic_conflict, the three
state_pseudostate_chain_* fixtures, TestExploreDynamicChoiceBranches and
robustness_test.go:state_choice_without_an_enabled_branch; every other state_choice_* fixture
kept its outcome.
Closed by its design record, exception-handlers.md: not a SysML v2
construct. The compliance mapping had carried UML's RaiseExceptionAction/ExceptionHandler
as "spec exists, needs exception propagation", the last UML-referenced action item still
presented as implementable. The record finds no spelling for raising, catching or propagating in
SysML v2 §7.17 (the action kinds are send, accept, assign, terminate, if, the loops and
the control nodes), no metaclass in §8.3.17 or the reflective SysML.sysml metamodel, no base
type in Actions.sysml, and in KerML a Performance that ends but never fails
(Performances.kerml); no OMG corpus model writes one, and the pinned pilot's SysML.ecore (175
classes) has no such class — which is also why the fUML referee files fUML's
exception model not-expressible. The need is met by constructs the runtime already executes: a
step reports its failure on an out parameter and a decide routes on it, or the step sends a
failure signal to an accept forked beside it whose branch terminates the work — §7.17.10's
MonitoredActivity — both run to completion under bin/sysml in the record; a failure the model
does not spell is a typed error at the boundary (ErrDivisionByZero, ErrAcceptDeadlock,
ErrTerminateTarget, …), never a panic, and an error verdict under a verification case.
No AST, IR or runtime change follows; the behavior guide's Terminate section teaches the
by-signal shape. A later SysML v2 revision adding an action kind for exceptions reopens the item
with a spelling to implement.
Expression evaluation is the most externally refereed part of the runtime: when
cmd/pilot-exec-diff was re-run for the review (2026-09-02, at 1f136d27 — a snapshot of that
round, not the current baseline), 55 cases agreed with the pinned pilot, 1 agreed in kind only, 1
disagreement and 2 errors of ours were adjudicated, and 35 cases the pilot cannot evaluate; 70 of
the 71 grammar forms in KerMLExpressions.xtext have a corpus witness (only % lacks one).
Scalars, Booleans, strings, enumerations, quantities, Complex (#777, #788), arrays, vectors and
vector quantities (#883), rationals (#876), occurrence lifetimes (#884), collection bodies and the
named reducers (->reduce '+', builtinControlReduce), indexing, qualified names, the lazy
conditionals, ??, the Kernel Function Library with overloads by argument type, the step and
element budgets, RDF expression trees and the native scalar/collection fast path are done. The
review after v0.4.3 found two defects, both landed (#794: x @ T on a value is istype, with
@@ still the metadata classification; #795: a feature named chain resolves as a name where the
lookahead does not establish the chain modifier — step chain … still does not parse and is a
separate parser item). Since then the 0.6.0 release closed the first two items of this track:
constructors evaluate in a value position (X1, #981) and a singleton sequence takes part in
arithmetic as the scalar (X2, for a feature's own value), 0.7.0 closed five more — casts (X3,
#115), * and .metadata (X4, #113), function values (X6, #122), the set kind and rank-n tensor
quantities (X7's value half, #121) and the static type of a collection body (X8's typing half,
#112) — and 0.8.0 closed X2's chain-read half (#164), evaluated the meta cast (#212) and typed
a bare feature reference, an untyped collection body and an argument of unknown type statically
(#174, #213, #241), and the last operator the
evaluator refused outright — the unary ~ — was adjudicated abstract-only rather than given a
value, KerML 1.0 §8.2.5.8.1 marking it undefined and the checker now warning on its use
(bitwise-complement.md). What follows is
measured against runtime/eval.go and bin/sysml at this baseline: each landed item is stated
with what it leaves, and what is still open in the track is X7's native layout, and X8's
two harness halves.
ast.ConstructorExpr parses with its positional and named arguments (#838, #875), types as the
definition it names with its arguments checked against that type, and since #981 evaluates in a
value position: runtime/eval.go dispatches it to runtime/signal.go evalConstructor, which
creates an object of the definition in the evaluating context with the arguments bound to its
features — positionally in declaration order (new Pt(1.0, 2.0)) or by name (new Pt(y = 2.0, x = 1.0)), both reading back p.x = 1.0, p.y = 2.0 — the same path send new Def(args) had
used for its payload. What depended on it moved: new SampledFunction(samples = (new SamplePair(…), …)) constructs and Domain reads it (L7), and an analysis case builds its own
records (A1). What remains is on the compiled side: the interpreted/native differential cannot
cover a constructor until N2.1 gives the compiled side a record type.
Landed in #164, completing what the feature's own value already did. KerML has no
scalar/sequence distinction: a one-element sequence is the value, and [0..*] features with
one value take part in arithmetic. Both directions hold for a feature read directly: attribute xs : Real[0..*] = (2.5); gives xs + 1.0 = 3.5, and the mirror, attribute ys : Real[0..*] = one; with one : Real, reads 1.0 where a sequence is expected. The half that was open at
v0.6.0 — a singleton read through a feature chain on an object, sp.domainValue on a
constructed SamplePair (a feature redefined without its own multiplicity, so it inherits
KeyValuePair::key's [0..*]), arriving as the sequence [0.0] and refused by every operator —
is closed by one binding rule serving every place one value is taken: an operator's operand, a
[1] parameter of a user calc or a library function, a cast or classification operand. A
collection of several stays what it is and is refused as before (type mismatch, multiplicity violation), an empty one still holds no value, and the multiplicity checks are not weakened:
attribute two : Real[2..*] = (1.0); is still refused by the checker as 1 value(s) bound to a feature with multiplicity lower bound 2 before anything evaluates. calc_sample_pair_arithmetic
pins the chain reads, and interpolateLinear runs (L7). What it leaves, by design: nothing — the
row in spec-compliance.md is faithful.
Landed in #115. x as T selects the values of x that T classifies, in order, and answers
the empty sequence when none does — against bin/sysml at this baseline 2.5 as Integer is ()
and 2.5 as Real is 2.5 — with the semantics the metamodel gives: not a conversion
(ToInteger and its siblings remain the library functions that convert). Scalars are judged by
their magnitude against the ScalarValues hierarchy, quantities by whether their unit is
commensurable with the target's dimension, arrays, vectors, tensors, measurement references and
frames by shape, units and frame, objects and enumeration literals by the types they carry; a
composed target (union, intersection, difference, nested to any depth) classifies as its operands
do, and every type a value's feature is declared with counts among the types it is of, so a
custom scalar subtype keeps the values declared with it. as, istype and hastype are
model-level evaluable, so a metadata body may bind x = 1 as Integer. What it leaves, by design:
a target that neither a value's types nor its content settles is reported rather than the value
being silently dropped. Five cast cases were added to the pilot execution referee
(cmd/pilot-exec-diff) with the change.
Landed in #113. * in an expression position evaluates to the unbounded value: it exceeds
every finite Integer, Real and Natural (* > 1000000 is true against bin/sysml), equals
itself, prints as * in the REPL and in traces, and crosses gRPC on its own Value.infinity arm
under the infinity_value capability, never as the string "*". elem.metadata yields the
metadata annotating the element as a sequence of metadata instances in declaration order, with
the values the annotation body binds and the metadata type's defaults where it binds none; an
element with no metadata yields the empty sequence. What it leaves, by design: arithmetic over
* is refused with a typed error naming the operation rather than answering an infinity or a
NaN, and reading .metadata off a value rather than an element is a typed error.
Landed in #211, #238 and #239. all T (KerML ExtentExpression, BaseFunctions::'all')
answers the instances of the named type as an ordered sequence, statically T[0..*], over the
whole loaded model: every object the run holds and every namespace-level object usage of every
document that may hold a T — an imported package's, an un-imported one's and the standard
library's alike, so all Clock answers Time::universalClock — materialized as the extent is
taken, with a variation's extent its variants, an enumeration's its literals, and a usage of
several occurrences (part wheels : Wheel[2];) denoting its lower bound of objects. What it
leaves, by design: a data type's extent (all Integer, all Point) is refused with
ErrUnboundedExtent, since a run creates no data values to enumerate; a namespace-level port or
open-count collection that may hold a T refuses the extent with ErrExtentUnavailable naming
it rather than answering short; all T is never model-level evaluable; and the native compiler
keeps refusing all, a compiled program having no run whose extent it could report. The
named-reducer half landed earlier: ->reduce '+' and the other named spellings resolve to the
library function by name through L4's dispatch (runtime/collections.go builtinControlReduce;
conformance calc_library_complex_sum_real_axis exercises the Real-axis case). The extent_*
conformance cases pin the operator; the query side of a runtime population is Q2's (#267).
Landed in #122. A calc definition, a calc usage awaiting an input, or an in calc parameter
named where a value is expected is a function value — the calc together with the scope and object
it was read in — invoked through a calc-typed parameter, passed positionally or by name, read off
a part, returned from a calc, compared and adopted: a model's own Apply(Sq, 3.0) with in calc f { in x : Real; return : Real; } answers 9.0 against bin/sysml, SampledFunctions::Sample
samples a user calc (L7), TradeStudies::evaluationFunction binds one (A2), and a library
function the runtime implements (RealFunctions::sqrt) is a value too. A calc declared in a
behavior body closes over the innermost active run of that behavior alone, never a caller's
parameters. Calling a non-function, an arity mismatch and an unbound calc parameter are typed
errors. On the wire Value.function carries the calc's qualified name and the id of the object
it was read off, under the function_values capability the five clients expose as a typed value.
What it leaves: a function closing over a behavior body's bindings crosses the wire as an
unsupported null, since no name reconstructs it; one read off an object is refused as an argument
to a later call, since that object lived only within the response that sent it; and native
compilation refuses a calc that binds or applies a function value with a typed error (confirmed
against bin/sysml -compile at this baseline). It is not an arbitrary closure over statements,
and was not meant to be.
The representation decisions are all taken. Since #883 a Collections::Array is a ValArray
with its dimensions and row-major elements, a NumericalVectorValue a ValVector, and a
VectorQuantityValue a ValVectorQuantity with a unit per axis. Landed in #121: where the
Kernel Data Type Library declares a collection's elements unique and unordered — Set,
UniqueCollection, Map — the runtime holds a set value, each member once, size counting
members, equality ignoring the order the members were written in (confirmed against bin/sysml),
contains/containsAll as membership, and one canonical enumeration order when an ordered
operation consumes it; what the library declares ordered or nonunique is unchanged. A
TensorMeasurementReference with three or more dimensions builds a tensor of that rank whose
# takes one index per dimension, with the wrong index count, an index out of range, a non-Integer
index, a component count off the flattened size and arithmetic between two shapes each a typed
error; the shape survives +, - and the scalar multiplications. Both cross gRPC whole on set
and tensor_quantity arms under the set_values and tensor_values capabilities, decoded by the
five clients into native types that check their own invariants. The RDF half is resolved by
design: the mapping states a model, never an evaluation of it, for every value kind — an Array,
a vector, a quantity with a unit and a scalar alike are written as the expression that values the
feature, and internal/translate/export does not reach the runtime at all
(tests/hygiene/layering_test.go forbids the import) — so a set or a tensor needs no literal form
either. A Set-, UniqueCollection- or Map-typed feature's elements and a rank-3 or rank-4
TensorMeasurementReference export as standard OperatorExpression/LiteralExpression/
FeatureReferenceExpression/InvocationExpression trees under sysml:type, round trip exactly
with sysx:sourceText stripped, the structural predicates carry the round trip (operator,
function and referent each break it when removed, as do both operand routes together), the
model read back evaluates to
sets equal regardless of the order their members were written in and to tensors of the same shape
and components, and no sysx: term beyond what every other expression already uses appears
(set_tensor_rdf_test.go). What remains open is the native half only: neither value compiles
natively — sysml -compile refuses a calc that uses one with a typed error naming the type
(confirmed at this baseline). Still last in the track, and now only that half.
The typing half landed in #112: a collection operation's static type follows what its
declaration hands through, not the element type of the collection — xs->collect { in x : C; x.mass } and xs.{ in x : C; x.mass } are typed by the body's result, a nested collect by its
innermost body, xs->collect f by the named function's result, select/reject/selectOne
keep the elements of xs, reduce follows its reducer (or the one element a one-element
collection hands back unreduced), forAll/exists stay Boolean, and a body whose result
cannot be typed keeps the library's Anything. Value conformance, bound values, invocation and
trigger arguments and enumerated values are judged by the specialized type, so accept when counts.{in n : Integer; n} is refused where it was silent and when counts.{in n; n > 3} is
accepted where it was refused; xs.?{…} types as xs->select {…} does, and sibling element
types share their nearest common supertype. (#903 had made the typer terminate on the Apollo 11
mass rollup without changing what it answered; this changes the answer.)
The two harness halves are open, unchanged since v0.6.0. Interpreted and compiled
evaluation are held equal by one differential over the compiled subset; the pilot differential
(cmd/pilot-exec-diff) still needs its normalization of numeric spellings and an adjudication
file the harness reads to say why each remaining disagreement is ours or the pilot's — the
adjudications live in prose in pilot-execution-referee.md, and what cmd/pilot-exec-diff
gained since v0.6.0 is cases (#115's five casts, #235's stdin-fed engines), not the
normalization; and the RDF expression trees have no round trip of their own — a tree is written,
read and rewritten only as part of a whole model, internal/translate/export having gained #121's set
and tensor values, the canonical predicate order (#166) and the library copies' normative identity
(#276) but no tree-level harness. Two hygiene items that make every X change measurable; do them
first when the track is next picked up.
Four query surfaces exist and are landed: the standard API Query over a project's elements
(internal/frontend/grpc, the OSLC query grammar with its diagnostics — #798, #812), the native document
query (query def with parameters, planned by internal/ir/queryplan and run by
internal/doc/queryexec, from the CLI, the REPL and gRPC), Evaluate/-eval/%eval in, and the
solver's solve. Two of the four answer from the runtime: Evaluate/-eval/%eval in since 0.8.0 —
all T enumerates the objects a run holds (X5) and %eval in #1 reads an object's current values
— and the document query since #267 and #293 on develop, whose rows may be the objects
%instantiate, -instantiate and the service's Instantiate create, whose operations read what
they hold now, and whose parameters bind to a held object from the REPL, the CLI and gRPC alike.
The API Query reads the model alone; the solver decides satisfiability rather than reading
what holds, though %solve pins the values a matching held object has before synthesising the
rest. The document query reaches the state a session's objects are in and the trace -trace prints
since #359 (Q3). Nothing of the track is open.
One page distinguishing the four: document queries over elements, the API Query over a project,
Evaluate over one expression in one scope, and solve over constraints; what each returns, what
each cannot see, and where runtime queries (Q2–Q4) sit. Done: the document-generation manual's
Which query is which is that page — one row per surface with what it
reads, what it returns, what it cannot see and its entry points, then a section apiece — and the
manual's interfaces and outputs
chapters carry the runtime side of the document query: a parameter bound to a held object by name,
#id or path from the CLI, the REPL and the Python client (ObjectRef), -instantiate beside
-render-document, Verdicts rows (DocumentVerdict in the Python client) and the data-object,
data-verdict and data-path attributes the HTML carries. Each later item points at that page
rather than re-explaining the boundary.
all Vehicle (every object typed by Vehicle in the session), and the collection operations over
it — all Vehicle->select { in v; v.mass > 1000 [kg] } — are the expression form of a runtime
query, and that half landed with X5 (#211, #238, #239) in the tag: the extent is a value,
statically T[0..*], the collection operations run over it, and it is taken over the whole loaded
model. The query side landed in #267, on develop after the tag: a document query's rows may
be the objects a session holds (queryexec.ValueObject, a *runtime.Instance under the path the
session reaches it by), every operation that takes an element row takes an object row and reads
what it holds now — OwnedElements/Descendants/Ancestors walk the objects held and holding,
WhereType tests the object's types, WhereName its path, WhereFeature/Project/OrderBy/
Column its current values — DocumentQueries::Objects(type = T) is the population as a query
operation, refused outside a session, %run-query/-run-query bind a parameter to the object held
under a usage's name, to #id or to a path (car.wheels[2]), -instantiate is accepted beside
-render-document so a document reports car.wheels[2]'s current value and not its declared
default, and objects render by path in Markdown, HTML (span.sysml-object, data-object) and PDF.
RelatedElements stays over elements and refuses an object row. Its companion #263 validates an
object as a whole (A7) over the same carrier walk, and #289 puts that sweep in a query:
DocumentQueries::Verdicts(source, kind) answers one row per assertion about each row's object —
the held object, or the element's declared one — and the objects it holds, the assertion as the
row's element with kind, carrier, path, verdict, condition, reason and verification
read by WhereFeature, OrderBy, Project and Column, so "which requirements does this car
violate" is a WhereFeature over Verdicts in a document; verdicts render in Markdown, HTML
(span.sysml-verdict) and PDF and cross RunDocumentQuery as the verdict arm of
DocumentValue. The last piece, the gRPC binding of a held object as a query parameter,
landed in #293 on develop: Instantiate keeps the object it creates, in one runtime per
cached model, for as long as the model stays cached (instantiating the same usage again denotes
the new object and keeps the earlier one by id); RunDocumentQuery binds a parameter to such an
object through the object arm of DocumentValue — a DocumentObject naming it by
instance_id, by path in %run-query's grammar (car, Garage::car, #2, car.wheels[2])
or by both — and answers an object row or an object-valued cell as the same arm with the
object's id, path and usage; RenderDocument renders over the same population; a binding while
nothing is held or naming an unknown object is NOT_FOUND, a path that reaches no object, an
out-of-range index or an id its path disagrees with INVALID_ARGUMENT;
OPENSYSML_GRPC_MAX_HELD_OBJECTS bounds what one model holds and an operation that would pass it
fails whole with RESOURCE_EXHAUSTED; the Go client binds with ObjectByID/ObjectByPath and
the Python client with ObjectRef, both decoding the object cell, and the Node, Java and Rust
clients carry the regenerated stubs. Nothing of Q2 is left; the rows in spec-compliance.md say
so.
"Which state is #1.lp in?", "which objects are in run?", "what did #1 accept between
t = 1 [s] and t = 2.5 [s]?" — the state executor and the trace had the answers
(StateExecutor.ActiveStates, the event log -trace prints) and no query read them. Q3 is a
runtime query vocabulary over current state and over the trace as a time-ordered relation, with the
same filter forms as Q2, so the trace stops being something one reads by eye. Landed in #359 on
develop after the tag, as three operations of DocumentQueries beside Q2's Objects and
Verdicts. States(source) answers state rows: one per active leaf of each source object's
machine, every orthogonal region included, with the object, the machine, the leaf's name, its
dotted statePath under the machine, the region it is active in and the enclosing composite
states, read from StateExecutor.ActiveLeaves as the configuration stands now. InState(name) is
the inverse — the held objects whose machine is in the named state, by leaf, by enclosing state or
by dotted path — over the same population all T and Objects read (X5, Q2). Events(source, kind, since, before) answers event rows: the trace in the order the run made it — accepts,
sends, transitions, state entry, exit and do steps, choice draws with their alternatives and the
one taken (region order and due order among them), unevaluable guards — each with its instant on
Context.Clock() (A5), its object and machine, the states it touches, its payload and the line
-trace prints; kind keeps one or several kinds and [since, before) is inclusive at the start,
exclusive at the end, in the clock's unit or as a duration. The representation queried is the one
the runtime records: runtime.TraceRecorder keeps a typed TraceRecord per event and the printer
writes -trace's lines from those records, so the two cannot disagree and the printed trace is
unchanged. WhereFeature, WhereName, WhereType, Project, OrderBy and Column read state
and event rows as they read object and verdict rows; a model-only operation given one, an object
exhibiting no machine, a state no machine declares, a session recording no trace, a bound that is
no instant, an empty or backwards interval, and an interval reaching records a bounded trace has
dropped are each a typed error. The rows cross every surface Q2 has: %run-query and -run-query
(which runs after -state, -action and -advance, so it reads the run's end), the Markdown, HTML
(span.sysml-state, span.sysml-event) and PDF renderers, and RunDocumentQuery as the state
and event arms of DocumentValue, decoded by the Go and Python clients and carried by the Node,
Java and Rust stubs; OPENSYSML_GRPC_MAX_HELD_EVENTS bounds the trace a served population keeps.
The manual's Which query is which and the query cookbook teach the
forms; the rows in spec-compliance.md carry the status. Nothing of Q3 is left.
A query def parameter with a default (in limit : Natural = 10;) was refused at execution with
relies on a default not retained in the plan — the plan recorded HasDefault and not the
expression. PR #849 (landed) keeps the
compiled default in the immutable plan with its declaring query, evaluates it in the declaring scope
once per execution (not per row), honours inheritance and redefinition (in redefines threshold default "5"), lets an explicit binding override it, refuses an unrepresentable default at planning
and a self-referencing one as a composition cycle; ErrorDefaultUnavailable is gone. Verified at
this baseline over docrender/testdata/defaulted_queries.sysml through -run-query and
-render-document. One adjacent rough edge, not a defaults defect: -run-query "Q threshold=1"
types the bare 1 as an integer and is refused against a String parameter — quote it
(threshold="1").
The Systems Library's analysis vocabulary loads and type-checks — AnalysisCases, TradeStudies,
StateSpaceRepresentation, SampledFunctions, VerificationCases — and the runtime executes
calcs, constraints, requirements, actions, state machines and, since 0.6.0, an analysis case
(A1, the keystone the review after v0.4.3 found missing). Each simulation capability people
expect from "SysML v2 execution" hangs off that keystone, and v0.7.0 shipped four of the five:
verification verdicts (A6, #117), parameter sweeps (A3, #118), the trade study (A2, #133) and the
shared clock (A5, #136). The two that were open at the tag landed on develop after it: A7, one
verdict for every assertion an object carries, in #263, and A4, the state-space runner A5
unblocked, in #296. Nothing in the track is open. Every item below is self-assessed (the pilot
executes none of this); the runner's two stated limitations are its own.
Landed in PR #979, shipped in 0.6.0. An
analysis definition or usage runs as the calculation it is: the subject is an in parameter
(bound by the usage, by the object the run is asked on, or from the enclosing case; a run with no
subject is refused naming it), the body's action/perform/nested analysis steps are one
lower.Block over the action graph they state (then, first, forks, joins, decisions, merges;
declaration order where none is stated) run by the action executor, out and return are
evaluated in the case's frame with their units, and the objective and every assert constraint
are checked afterwards by the requirement engine as satisfied / not satisfied / undecided.
-analysis Pkg::Case/%analysis beside -calc and -action, the RunAnalysis RPC with
Connect, Go and Python clients, and reads of an analysis usage's outputs as features
(An::shipCost.total, holder.inner.total, attribute :>> x = a.result;) with memoization and
invalidation are in; the follow-ups in the same release bind an objective's requirement subject by
keyword alone, read a case's result by its qualified name, report a recursive step once, keep
later objectives in position and hold an actor bound without :>> to the actor it inherits.
What A1 deliberately leaves, each stated where it is owned: -calc/%calc/EvaluateCalc still
refuse an analysis by kind and say to run it as one — that is the contract, not a gap; the
training corpus's 33. Analysis fuel-economy cases and the Analysis Examples corpus state their
step outputs only through assert constraint (solveForPower.power has no computation to run),
so they end in the typed no value for feature refusal — the solver's territory, not the
executor's. Two of the things it left at 0.6.0 have since landed: a verification case body is run
and its verdict reported (A6, #117), and a calc named as the value of an in calc parameter is
a function value (X6, #122), which is what 10c-Fuel Economy Analysis.sysml needed admitted for
its bodiless calc cityScenario (whether that case's steps then run to a value has not been
re-measured at this baseline); a trade study's iteration over its alternatives is A2 (#133). What
the corpora answer today: the pilot's 10d-Dynamics Analysis.sysml runs
on a supplied subject and inputs (accelerationProfile = [0.01, 0.01998…]), and
10a-Analysis.sysml binds an untyped part vehicle to a Vehicle subject and states no mass
values, so it is refused at the binding (typed) and a copy with typed, valued parts answers
200 [kg]. Compiling a case (N2.1) was never part of this item.
Landed in #133, on X6's function values. A TradeStudies::TradeStudy definition or usage run
through -analysis, %analysis, RunAnalysis or a sweep executes the library's own expressions
rather than a special case: the subject binds studyAlternatives, the case's evaluationFunction
binds tradeStudyObjective.eval as a function value, MinimizeObjective/MaximizeObjective
compute best with ->minimize {in x; eval(x)}/->maximize, the inherited require constraint { eval(selectedAlternative) == best } is checked as the objective's condition, and
selectedAlternative is the first alternative ->selectOne finds it holding for. Four general
rules carried it and are now the runtime's: a domain library's calc executes from its text as a
model's does; an inherited expression reads a feature through the running case's redefinition of
it; a requirement usage applies as a predicate with its subject as its first parameter; and a
redefinition stating no multiplicity inherits the redefined feature's. Every application of the
case's calc is reported with the run, in subject order, marked [selected] and [tied], and
crosses gRPC as RunAnalysisResponse.evaluations/SweepRow.evaluations under the
case_evaluations capability, read by the Go (Analysis.Evaluations/Selected()), Python
(AnalysisResult.evaluations/.selected), Node, Java and Rust clients. What it leaves, by
design: an alternative whose evaluation fails, an evaluationFunction without a body and a
subject listing no alternative are typed errors that leave the objective undecided, never a
fabricated pick; and %optimize <case> (experimental, the z3 (minimize …)/(maximize …)
search over the constraint model) now refuses an objective whose eval is bound to the case's
own calc and points at the analysis run — a study whose alternatives are enumerated by a solver
rather than a list stays %optimize's.
Landed in #118. sysml -sweep "<param>=<from>..<to>[:<step>]" runs the -analysis case or
the -calc once per value of the range — several -sweep flags run their cartesian product, the
first varying slowest — and prints one row per run (the inputs bound, what it computed, the
objective's verdict, the wall time) as a text table or inside -json; -samples <n> -seed <s>
draws n uniform values from each range deterministically from the seed and echoes the seed with
the table; %sweep and %samples do the same in the REPL, RunSweep over gRPC, Model.run_sweep
in the Python client. A run that fails is a row carrying its error, not the end of the table, and
OPENSYSML_MAX_SWEEP_RUNS bounds a table. What it leaves, by design: a range between Reals with
no step, a step of zero or of the wrong sign, a parameter the target does not declare or the
arguments already bind, and a distribution asked for by name (only the uniform draw exists) are
typed refusals. Orchestration over A1 with no new semantics, as planned; a document query over the
table is Track Q's. #344, on develop after the tag, adds what the item left out: a model states
its own odds through two non-normative libraries (Stochastic::Probability weighting the
successions out of a decision, validated at lowering; RandomFunctions for uniform,
uniformInteger, triangular and normal), on a random stream of its own apart from the
token-shuffle stream, every draw recorded in the witness so %replay reproduces a run, and
%runs / -runs <n> -seed <s> run an action n times on the sweep machinery and report each
observable's min, mean, max, percentiles and histogram. The scheduling choice points stay
unweighted and explore still enumerates weighted branches as a set.
StateSpaceRepresentation declares the protocol (a state vector, its derivative, an output) and
at the tag nothing integrates it: no time-stepping runner, no integrator (the RK4 lunar-descent
conformance case is a calc that hand-rolls its stages), no zero-crossing detection to hand an
event to a state machine. Landed in #296 on develop after the tag, on A5's clock. An action
specializing ContinuousStateSpaceDynamics or DiscreteStateSpaceDynamics runs as a fixed-step
state-space simulation: the bundled StateSpaceIntegration library adds FixedStepDynamics
(timeStep, an optional stopTime, the time the run writes), the integrators Euler and RK4
that a model binds to getNextState's integrate (RK4 when it binds none — the integrator is
the model's choice, not a run option) and the ZeroCrossing event; discrete dynamics step by
getDifference. Each step advances the shared clock, so a state machine exhibited beside the
dynamics sees the same time, its accept after/at triggers fire in step order, and a step and
a trigger due together are a due order choice point the scheduling policy decides. An event occurrence typed by ZeroCrossing posts an event of its type when its guard changes sign at a
step, which a machine's accept takes, and ends the dynamics when terminal. The trace records
state: <action> t=<instant> x=<state> y=<output> per step and the run reports stateSpace,
output and time as the action's outputs. What the runner refuses, typed and naming the action
and the member at fault: a state, input, derivative or output that is not a vector, a protocol
calc left abstract, an integrator the runtime does not provide, a step absent, zero or negative,
a state a step leaves non-finite. Its stated limitations: a crossing is located to the step
boundary and not bisected within the step, so a guard that crosses and re-crosses inside one step
is not seen; StateSpaceItem and a StateSpaceEventDef that is not a zero crossing are not
executed. The OMG example State Space Representation Examples/EVSample1.sysml runs through the
runner once a model of its shape also states a timeStep, the library itself stating none. L7's
census reads StateSpaceRepresentation at 6 of 17 with the runner in, the rest the abstract
protocol probed directly. The lunar-descent case stands as it was.
Landed in #136. Simulation time is a property of the runtime context, no longer of one state
machine's executor: every state machine and action a context runs reads the same clock
(Context.Clock(), in SI::s), so two machines materialized in one context share time and a
nested performance runs on the enclosing clock. An action body waits on it — accept after <duration> parks the token until the clock has moved that far, accept at <instant> until it
reads the instant — where before the ordinary action path refused a time trigger (a time-triggered accept with no clock; that robustness case is replaced by action_accept_time_waits
and clock_advance). Context.Advance(duration) runs every state event, action token,
change-condition poll and do round due up to the new instant, instant by instant, within the
budgets; -advance no longer needs -state and runs the invocation's -action and -state
behaviors together on one clock; %advance moves the session runtime's clock, so an %action
and a %state debugger both move. Which executor runs first when several are due at one instant
is a choice point, due order, drawn by the scheduling policy (S3) — the executor started last
first under reverse, the first started under declared, a draw under seed:<n> — so it is
enumerated by explore (S4) and one executor alone due is no choice and is not reported, leaving
every single-behavior result and trace unchanged. ExecuteActionResponse and
ExecuteStateResponse report final_time under the final_time capability. What it leaves:
A4's runner is not yet on the clock, since it does not exist; Q3's queries over the trace it
changed landed in #359.
Landed in #117. sysml -analysis, %analysis and the RunAnalysis RPC accept a
verification def or verification usage and run it as they run an analysis case — the same
lowering, subject and input binding, and step execution — and report the VerdictKind the body
produced: pass or fail as the library's own VerificationCases::PassIf computes it, a
VerdictKind literal the body binds as it stands, inconclusive for a body that produced no
verdict, error with the message for a body whose run failed. The body verdict is reported
beside requirement satisfaction, not instead of it: -requirement, -satisfy, %requirement,
%satisfy and the VerifyRequirement/VerifySatisfaction RPCs add one line per verification
case verifying the requirement, and what the requirement engine decided and the exit status are
unchanged. Over gRPC the verdicts are verification_verdicts fields under the
verification_verdicts capability, each carrying the requirement_id it was reported for;
-json reports them under verifications; the Go and Python clients expose them as
Verifications/verifications. What it leaves, by design: a case performed as a step of another
is reported on its own, marked as a subcase, since the library states no roll-up.
A7 — an object validates as a whole: every assertion it carries, in one verdict (landed, unreleased)
Landed in #263, on develop after the tag, as written below. runtime.Context.ValidateObject
walks an object's part tree — the composite features Instantiate materialized, collections
included, one-based paths such as wheels[2] — and reports one ObjectVerdict per (assertion,
carrier): the asserted constraints and invariants the carrier's type declares or inherits, the
requirement usages it carries and the satisfy assertions whose subject it is, each holds,
violated or undecided with the reason, then one verdict about the object itself, valid only
when every line holds and the walk was complete — a tree cut at the depth bound, a value that
could not be read, or an object no assertion is about leaves it not shown valid. Surfaces:
-validate=<object> (bare -validate keeps its meaning), %validate <object>, the
ValidateInstance RPC under the verification capability with Verdict.instance_path and a
summary verdict of kind object, and ValidateInstance/validate_instance in the Go and Python
clients. The evidence is the instance_validate_* conformance fixtures: three depths and a
collection part, a violated nested assertion, a same-type pair disambiguated by carrier, a
satisfy whose subject is a nested part, an undecidable condition, a shared object, an optional
recursion and a bounded walk. The rows in spec-compliance.md say so. The text below is the
design as it stood before the work began.
An object is checked today one assertion at a time. -instantiate car -constraint C,
%constraint C, %requirement R, %satisfy and the VerifyConstraint/VerifyRequirement/
VerifySatisfaction RPCs each name the element to judge, and the runtime finds the object that
carries it (Context.conditionSubject, carriersUnder): the constraint's or requirement's
conditions are then read from that object's feature values — car's 1800 kg, not Vehicle's
default — and structural conformance (multiplicity, type, uniqueness, binding conflicts) is
refused at materialization and on every write (checkAdmits, write_conformance.go). What no
surface offers is the question a modeler asks first: is this object valid? — every assert constraint declared on the object's type or on any part nested under it, every requirement usage
it or its parts carry, and every satisfy assertion whose subject lies in the tree, judged on the
object that carries each and reported together. -validate is not that: it reports only that the
model analysed cleanly and that the objects -instantiate asked for could be built, and the CLI
reference says so. Reaching the whole set today means knowing every assertion's name, one command
each, and %constraint C refuses when two nested parts both carry C ("check it on one of
them"), so a part wheels : Wheel[4] with an asserted constraint cannot be judged on all four in
one step at all.
The semantics are already the runtime's. An assert constraint is KerML's Invariant
(AssertConstraintUsage, SysML v2 §8.3.19.2; Invariant::isNegated, §8.3.21.10): a Boolean
expression that must be true of every instance of its featuring type, negated by assert not,
which is exactly what CheckConstraintOn evaluates for one carrier; requirements and satisfy
assertions have CheckRequirementOn and CheckSatisfactionOn likewise. A7 is the sweep over
them: walk the object's part tree (the composite features Instantiate materialized, collections
included), collect on each object the invariants and requirement usages its type declares or
inherits and the satisfy assertions whose subject it is, evaluate each on that object, and
report one verdict per (assertion, carrier) — holds, violated with the condition, or
undecided with the reason, each with the object's path (car.wheels[2]) — beside one overall
verdict, valid only when every line holds. Surfaces: -validate <object> (the flag already
exists; given an object reference it does this and keeps its present meaning bare), %validate <object>, a ValidateInstance RPC under its own capability, -json under validation, and the
Go and Python clients. Unasserted constraints (constraint c { … } with no assert) are declared,
not asserted, and are left out — they are what %constraint is for — and the verification-case
verdicts A6 reports beside a requirement are reported beside it here too. The pilot cannot referee
any of this, so the evidence is the conformance fixtures: one object with asserted constraints at
three depths and a collection part, a violated nested assertion, an ambiguous-by-name pair that
this sweep disambiguates by carrier, and a satisfy whose subject is a nested part. Depends on
nothing open; the population all T from X5 lets the same sweep run over every object of a
type rather than one tree, and it should share Q2's carrier walk rather than add one — which is
what #263 did, and #267 (Q2's query side) shares the walk.
The denominator is the pilot's 217 named validate* constraints, read from the pinned jar by the
census validation-constraints.md (#822) and re-audited row by row
against the code and the corpus (#900, whose gate now also checks that the function each row cites
exists and that each cited case belongs to its row). With #900 the census read 148 of 217
reported — 137 faithful, 11 approximate — 6 not implemented, 0 deliberate, 0 known failure and 63
unknown, against 143 / 133 / 10 / 68 at the tag; the adjudication of the unknown KerML rows then
moved it to 156 reported, and the census now reads 163 of 217 reported — 157 faithful, 6
approximate, 0 not implemented, 1 deliberate, 0 known failure and 53 unknown, with the last
not-implemented row, validateFeatureMultiplicityDomain, now landed and each remaining unknown row
citing why the pilot never reports it.
The oracle at the other end agrees: of 285
self-authored invalid models, the pinned pilot and we both reject 276 (3 of them only in strict
mode, by design), the pilot alone rejects 0, and of the 9 only we reject eight are control-node
succession rules the pinned pilot leaves unimplemented and one is a non-Boolean guard on an action
body's succession that the pilot's check leaves silent once the library types the guard
(pilot-rejection.md). Everything the previous baselines listed as open here
has landed:
- The census and its gate (#822, #900); negative cases, one minimal pilot-refereed invalid model per SysML constraint name the corpus owns or a stated reason none can exist (#831), and the KerML rejection cases (#817).
- The rules the sample found missing: overriding a bound (
=) feature value (#826, which also carries the=/default =distinction through the AST); control-node succession counts and placement (#832); trigger arguments —aftera duration,atan instant,whena Boolean — in validation and in the state executor (#833, #871);sendpayload,viaandtotyping and constructor arguments (#838, #875);enum def F :> E— an enumeration definition is a variation (#811) and an enumerated value is typed only by its enumeration (#907, with the cast and body-expression positions in #909). - Rows the census audit itself moved: cross-subsetting (#872), duplicate parameter bindings
(#875), owning-body membership (#874), initial values and
constanton variable features (#873), metadata typing andannotatedElementconformance (#881, #901, #905, #906), association arity and the type of a multiplicity bound (#882), requirement-member prefix metadata and short names (#868, #880), a chained generalization under the cycle guard (#902). - Adjudicated as not a defect: the inherited-name warning beside a typing error stays (#837); the pilot reports both.
What remains is the census's own tail, and it is the whole of the track: the 1 row marked not
implemented and the 53 marked unknown — a row is unknown when neither a pass nor a negative case
can be pointed at for the pilot's name, which is a gap in evidence before it is a gap in checking.
The order is the same as before: take the 1, then work the 53 down by writing the pilot-refereed
negative case first and implementing the rule only where the case shows we accept what the pilot
rejects; each change moves exactly its census row and make docs-counts regenerates the summary.
The 6 approximate rows (a check under our own wording that covers the pilot's constraint
without matching it) are honest as they stand and become faithful one at a time as their
wording is aligned.
sysml-grpc speaks gRPC, Connect and Connect-JSON over one port, with optional TLS, exact-origin
CORS, a health port and stdio; there are clients in Go (the public API), Python (opensysml on
PyPI), Node, Java and Rust, each with a worked tour. Verified at this baseline against the rebuilt
binary: plain HTTP/1.1 + JSON reaches every method (ParseSources takes documents, not
sources; Evaluate on 2 * 1500 answered {"result":{"realValue":3000}}), which means MATLAB,
R, Julia and C can integrate today with their HTTP and JSON libraries and no generated code. What
is missing is the contract that makes such a client correct rather than lucky, and a thin package
per language so nobody re-derives it.
There was no page a hand-written client can be built from: which field names (proto3
lowerCamelCase), which Value arm is which (intValue as a JSON string for 64-bit,
realValue, boolValue, stringValue, quantity with unit, enumeration by identity not display
name, instance references, Complex), how unset differs from absent and from no result, how a
diagnostic and a Connect error arrive, the model-hash lifetime, and how a behavior call and a query
are made. wire-contract.md is that page (#848, landed): every
example captured from the running service, every Value arm (eleven when it was written; the
infinity (#113), function (#122), set and tensorQuantity (#121) arms added since v0.6.0
are on the page), the Connect code table, the
Instantiate/ExecuteState/Verify*/Query answer shapes, and R, Julia, MATLAB and C
illustrations marked untested, linked from the transports, clients and API pages. Everything else
in the track reads from it.
One directory of request/response pairs — every Value arm, a diagnostic, a Connect error, a
behavior call, a document query — that the five existing clients and any new one replay, so "the
client decodes the contract" is a test and not a claim. Depends on I1; small.
Each is a few hundred lines over the language's HTTP+JSON: connect, parse, evaluate, query, run a
behavior, decode Value by the I1 rules (64-bit integers, quantities, enumerations, unset), and
surface diagnostics as the language's errors. Each ships with the I2 fixtures as its tests and a
page on starting sysml-grpc (binary provisioning from the release, the flags, TLS, the health
check). Not started; each is about a session once I1 and I2 exist, and they can go in parallel.
Publishing (CRAN, the Julia registry, File Exchange) is account-gated and goes with R2.
C is two different things. A C client is I3 for C — an HTTP+JSON client over libcurl and a
JSON library, and the natural base for anything that embeds by FFI. A C ABI is in-process:
calling a compiled calc (N2.4) or, later, an embedded state machine (M4) through a stable header
with no service. The client depends only on I1/I2; the ABI depends on N2.4's design and is the same
artifact as M4's host interface, so design it once and let the three consumers (host tools, the C
client's optional in-process mode, the embedded target) restrict it. The client comes first.
conformance/ is written as the contract between sysml-grpc and its clients, and that is the
only direction its runner exercises: tools/cmd/conformance builds ./cmd/sysml-grpc (or takes
-binary), starts the process itself and drives it over the three protocols. Nothing runs it
against a service that is not this repository's Go binary, so a second implementation of
sysml.proto — a kernel in another language behind the same clients — has no way to state how
conformant it is. Two changes make the suite that statement. First, an -address mode that
speaks to a service already listening rather than spawning one, reporting the capabilities it
advertised and skipping by requires_capabilities exactly as the client runs do (-allow-skips
already decides whether a skip is a pass). Second, a wider corpus: the thirteen scenario files
cover one or two calls per RPC, which is enough to prove a client decodes the answer and far too
little to prove a kernel computes it. The execution conformance cases under
internal/exec/runtime/testdata/conformance/ — a model, a call, an expected result — are the same
shape as a scenario and are the deepest semantic oracle the repository has; generating scenarios
from them (the .expected.json becomes the response) turns the wire suite into a kernel suite
without writing a second corpus, and any implementation's report then reads as a fraction of the
same cases the interpreter passes. Depends on I1 and I2; the generator is a session, and it is
refereed by running the generated scenarios against sysml-grpc itself, which must pass them all.
Every value the runtime produces comes from the model text and the bundled library. A calc
evaluates from its own body; a bodiless library function reaches a Go implementation only through
builtinFor (internal/exec/runtime/builtins.go), which requires the symbol to be
library-declared (libraryDeclared in library_functions.go asks the index) and then looks the
qualified name up in a table compiled into the binary. A feature without a value expression is
unset, and stays unset. An accept fires only for events the model itself sends, or that a host
injects by hand through StateExecutor.SendSignal, the REPL's %state commands or the
ExecuteState request's event list. The arguments to Evaluate, EvaluateCalc, ExecuteAction
and RunAnalysis are the entire surface through which anything outside the model reaches inside
it, and none of it survives the call. There is, in short, no way for a model to say this function
is computed elsewhere, this value is read from there, or these events arrive from that, and
no way for a host — the Go API, a service client, a REPL session — to supply the elsewhere.
That is the gap between an execution engine and a digital-twin substrate. The identity metadata
(ElementId, ProjectRef) says where an element lives; the Flexo interop says where a graph
is stored; neither says where a value comes from or who computes a function. The track adds that:
a notation for declaring an element externally bound, a provider contract the runtime consults at
the points it already dispatches, and the same contract over the service boundary so a client in
any language can be the provider. It deliberately does not add a scripting language, a plugin
loader or a foreign-function interface to the interpreter: the model names the binding, the host
supplies it, and the seam is the one the runtime already has.
A Bindings package under OpenSysML Libraries/, beside IdentityMetadata, as standard
user-defined metadata so every conforming tool reads a bound model as an annotated one: an
@External on a calc or function definition whose body is absent (the host computes it), an
@ExternalValue on an attribute or item usage with no value expression (the host supplies it,
once or on every read), and an @ExternalEvent on an event or signal definition (the host posts
it). Each carries a binding : String the host resolves and nothing else — the notation says
that and which, never how. A constraint-tier pass checks the annotated element is bindable:
the function has no body, the feature has no value, every parameter and result type has a
Value arm on the wire (I1), and an @External function is not also implemented by the library
table. The RDF mapping carries the metadata as it carries ElementId today, so a bound model
round-trips. Not started; the design record states the metadata, the pass and the refusals
before any of it is written.
One interface, consulted at the three points the runtime already dispatches. Before builtinFor,
a function marked @External resolves to the host's Call(binding, args); when a feature marked
@ExternalValue is first read (its Materialized flag is the existing seam), the host's
Read(binding) supplies it; a state executor whose machine accepts an @ExternalEvent drains the
host's Events(binding) into its EventQueue under the clock it already runs on. The bundled
builtin table becomes the first provider of that interface rather than a parallel path, so there
is one dispatch and not two that drift. A bound element with no provider is a typed refusal
naming the element and its binding, never an unset value; a provider's error is the calc's error,
with the binding in the message; every host call is a traced step, and the trace records the
value returned so a golden replays without the provider. On the Go API, opensysml.WithProvider
on the model handle, and a Provider interface a Go host implements in a page of code. Depends on
B1. Two sessions, refereed by conformance fixtures whose expected results only a fixture provider
can produce.
A Python, Node, Java or Rust process must be able to be the provider, not only the caller, or the
Go API is the only host that can bind anything. The direct shape is a bidirectional stream the
client opens (BindProvider), on which the service sends Call/Read requests and the client
answers; service-transports.md records that bidirectional
streaming needs HTTP/2 end to end and, in a browser, TLS, which is acceptable for a provider and
not for a page. The alternative — the service dialling a Connect endpoint the client hosts — has
no such constraint and no such streaming, at the cost of the client running a listener. Decide by
prototyping both against the Python client; either way the request and answer messages are
defined once in sysml.proto, the capability is bindings, GetServerInfo advertises it, and a
service without it refuses BindProvider as UNIMPLEMENTED like every other capability. The
conformance suite gains scenarios with a fixture provider the runner hosts, and the I2 fixtures
gain the provider messages. Depends on B2 and I1; a session for the transport decision and a
session for the clients.
B2 and B3 make a program the provider. Most values a twin reads are in a table, a file or a
service with a URL, and asking for a program to read them is asking for the same fifty lines in
every host. A small set of built-in providers, selected by the binding string's scheme — a CSV
or JSON file keyed by element, an HTTP endpoint returning JSON decoded by the I1 rules, and a
Flexo project's element values now that D9.2 reads one — configured on the sysml and sysml-grpc
command lines and refused by name when the scheme is unknown. This is the item that makes a model
with @ExternalValue runnable from the REPL against a spreadsheet with no host program at all.
Depends on B2; each provider is small and independent, and the Flexo provider is no longer gated
by D9.2, which is landed.
Query answers what is bound — every annotated element, its binding string, whether a
provider currently satisfies it and, from the trace, what it last returned — so a document or a
client can report a twin's wiring rather than infer it. The REPL shows the same through a
%bindings command and marks an unbound external element in %instantiate output instead of
showing it unset. Small; after B2, and it belongs with Q1's page that says which query is which.
A view's rendering is a view.Rendering — typed nodes (part def, state, fork,
decision, a lifeline), edges with labels, notices for what was not represented — and a
form is only a writer over it: text, markdown, mermaid and, since W1 and W2 landed,
dot and plantuml, chosen by -render-form, %render <name> <form>, the opensysml/render request the VS Code
panel makes, and the document renderer, which embeds the Mermaid form in HTML and rasterizes it
through mmdc for PDF. The tree, interconnection, state, action and sequence kinds all render — the
state rendering from the lowered StateGraph (regions, entry transitions, triggers, guards,
effects), the action rendering from the ActionGraph, the sequence rendering as lifelines and
ordered messages — so what is missing is not a diagram kind but the formats a rendering can
be written in, and the fidelity the one machine form allows.
Mermaid was chosen because it draws where the models are read, with no installation. The cost
is what its grammars cannot say: a flowchart has no fork or join bar, no swimlane, no pin, and
names a decision only by the diamond shape the writer does not yet ask for; stateDiagram-v2 has
no history pseudostate, no entry/exit/do compartments and no orthogonal-region separator beyond
--; sequenceDiagram has no found or lost message and no timing. Every one of those is a
notice in the rendering today rather than a drawing. Graphviz DOT and PlantUML both draw them,
both lay out large graphs Mermaid cannot, and both are what the documentation and publishing
pipelines this project is meant to feed already consume.
Landed — see view rendering forms. A DOT writer over Rendering
(internal/ir/view/dot.go): a rendering is a digraph, a node with children a subgraph "cluster_*" (a tree keeps containment as edges, as its Mermaid form does), direction maps onto
rankdir, the EdgeKind styles parallel the Mermaid arrows, and a state rendering draws its
states as rounded boxes with point/circle/doublecircle pseudo-states. Every identifier and
label is quoted through one helper; a // layout: header names the engine the file is written
for. The DiagramLayout geometry the rendering carries is written as Graphviz reads it — a
positioned node pinned with pos="x,y!" at its centre and sized in inches, a route as a pos
spline, the canvas as the graph's size, y flipped from the library's y-down pixels — and the
header then names neato (neato -n when every node is placed, -n2 when every edge is
routed too). The goldens beside every
*.mermaid.golden are checked by an in-test DOT syntax walker rather than by running dot -Tsvg, so no Graphviz installation is involved anywhere.
Still open from the original sketch, each a writer change and nothing else: the shape per action
node kind (diamond for a decision, a filled bar for fork and join), record or HTML-like labels
for a part with its compartments, note for a notice, and URL=/tooltip= from the origin every
node carries so an SVG rendered from the DOT links back to the declaration the way the LSP panel
does. The node and edge attribute lists are each written by one method (dotNodeAttributes,
dotEdgeAttributes), where the position and the route already join the label and style.
Landed — see view rendering forms. A PlantUML writer over
Rendering (internal/ir/view/plantuml.go), one grammar per kind: a tree is a class diagram
with containment as edges (as its Mermaid and DOT forms draw it), an interconnection nested
rectangle blocks with the Pilot's -[thickness=3]- connectors and dashed flows, a state
rendering the state grammar with composite states, [*] starts and PlantUML's pseudostate
stereotypes, and a sequence — the kind DOT has no grammar for — participants and -> messages
one for one with the Mermaid form. The action rendering takes the state grammar too, not the
activity syntax the sketch named: activity syntax is procedural and cannot hold an arbitrary
graph of successions and flows without inventing structure, so one grammar draws every action
golden losslessly, control nodes as pseudostates and flows dashed. Every file carries the Pilot's
Standard B&W style inline in a <style> block — the released PlantUML does not ship the
sysmlbw skin — honouring the three rules DOT could not (the usage corner radius, shadows off,
wrapWidth 300); the named palettes fill nodes with the same hex per node as the DOT form;
DiagramLayout geometry is kept as ' comments (PlantUML pins no position — dot does), through
the geometry-comment helpers the Mermaid form shares. Goldens beside every *.mermaid.golden are
walked by an in-test PlantUML syntax check; a PlantUML jar is never needed — OPENSYSML_PLANTUML_JAR
turns on an extra -checkonly pass when one is at hand.
Still open from the sketch, each a writer change: [H]/[H*] history where the rendering
produces a history pseudostate (today drawn by stereotype), state X : entry / … compartments,
notes for notices, and [[url]] hyperlinks from the origin — to land with the DOT URL= once a
writer has a stable URL for an Origin.
dot and plantuml join text, markdown and mermaid everywhere a form is chosen:
-render-form, %render, the opensysml/render request (the VS Code panel keeps Mermaid, which
it can draw in-process, and offers the others as save as), and the document renderer. Landed
— for dot with W1, for plantuml with W2, and the PDF rasterization and the panel's export in
#356 on develop after the tag: -render-form dot|plantuml
(-render-all writes .dot and .puml files), %render <name> dot|plantuml [palette],
"form" on opensysml/render, and -diagram-form dot|plantuml on -render-document
(%render-document <name> dot|plantuml, diagramForm on opensysml/renderDocument), which
writes every graph-shaped diagram block as a ```dot or ```plantuml fence in Markdown and
<pre class="dot"> or <pre class="plantuml"> in HTML — a render-time choice, not a model
attribute. The form lists in the CLI help and man pages, the REPL's completion
and the LSP's errors derive from Forms(), so the form reached every one. The gRPC
surface has no view-render RPC — only RenderDocument, to Markdown — so
the wire contract did not change; if one is added later it takes the form as a string the same
way -render-form does.
The PDF backend draws a DOT block through Graphviz (OPENSYSML_DOT, -Tsvg, honoring the
writer's // layout: line) and a PlantUML block through the jar (OPENSYSML_PLANTUML_JAR) as it
draws Mermaid through mmdc: optional tools located by environment variable, a block kept as
source under a notice when its tool is absent, the tests skipping with the reason where the tool
is missing and CI's pdf-toolchain job running them with the toolchain installed and reading
the rendered PDFs back (#477). The VS Code panel's export picks among the forms the server
advertises, so dot and plantuml are saved as .dot and .puml beside Mermaid. Still open
in the track is what W1 and W2 list as writer
changes: richer node shapes and compartments in both writers.
W1 landed first, being the smaller grammar and the one Graphviz-based pipelines want; W2 followed over the same node kinds and the sequence; W3 landed with each. Independent of every other track: nothing here touched the rendering model, only writers over it.
The question was whether OpenSysML models could run on a microcontroller under an RTOS, and what
"embedded SysML v2" would mean. The review's answer, from the code: not by shrinking the interpreter
— internal/exec/runtime depends on maps, allocation, big.Rat, and the parser and semantic
packages, and lower.ActionGraph/lower.StateGraph reference AST nodes, symbol scopes and
expression trees, so they are not closed artifacts that can leave the process (TinyGo is therefore
not a route). The route is Track N's discipline applied to behavior: a closed, serializable
behavior IR, an AOT C backend that emits static tables and no allocation, and a refusal —
typed, naming the construct — for any model the target cannot bound. What the runtime can promise
is bounded and reproducible execution; hard real-time guarantees (WCET) are properties of the
target, the compiler and the RTOS configuration, and the documentation must say so rather than
imply them. The design record for the track at the highest software class —
docs/internals/design/embedded-target.md — fixes the
freestanding C profile M2 emits, makes the IR's written semantics rather than the interpreter the
requirement basis, turns every admissible scheduling choice into a static refusal, lists the
artifacts under configuration control and restates M1–M6 as stages with exit criteria.
The lowered graphs, made self-contained: nodes, successions, guards, triggers, effects, states, transitions, regions, pins and connections with every reference resolved to an index and every expression carried as N2's typed IR, with no pointer into the AST or the symbol tables — so it can be serialized, diffed, and handed to a backend. Lowering to it must be lossless over the existing conformance corpus (the interpreter can run from it, which is the proof), and it is the meeting point with N2.6: the native track's action and state phases start here rather than duplicating it. This item gates everything else in the track.
From M1: state and transition tables, a static succession scheduler for the action graph
(a token count per node, no dynamic node creation), fixed-size event queues sized from the model,
static port/connection routing, expression evaluation through N2's C emitter, and no malloc
after initialization. A model that cannot be bounded — unbounded multiplicity, recursion the
compiler cannot bound, all T, dynamic new — is refused by name. Differential against the
interpreter over the state and action conformance corpus, exactly as N1 is refereed.
Every compiled model states its RAM (tables, queues, the state vector), its stack bound per entry point and its code size, before it is flashed; a budget file the model must fit is a build error when exceeded. This is what makes the target honest: the report is part of the artifact.
A generated model is a library the RTOS task calls: initialise, advance by dt (the shared clock of
A5, restricted to a fixed step), post an event into the fixed-size queue, read a feature. This is
the embedded restriction of the C ABI in N2.4/I4 — same header shape, no allocation, no callbacks
into the host except the ones the model declares. Designed with N2.4 and I4, implemented here.
One state machine and one action from the conformance corpus, compiled by M2, linked into a Zephyr
application, run under QEMU (qemu_cortex_m3) with the trace read back over the serial console and
compared with the interpreter's trace golden. CI runs it in a container with the Zephyr SDK; a
maintainer runs it on hardware. The claim "runs under an RTOS" is made only once this passes.
EmbeddedTarget metadata (the MCU, the step, the queue depths, the budget), and a small library
package of fixed-width numeric types and bounded collections the compiler recognises, so a model
says what it is for and the checker can refuse a Real where the target has no FPU. Declared in
the notation, checked by the existing passes, read by M2/M3. Last, because it depends on knowing
what M2 needs.
Four pieces of work proposed outside the tracks. The first two are landed on develop after
the tag; the third has its design and first step landed and three pull requests open; the fourth
is not started and waits on an input file.
The PDF path onto the HTML backend (landed, #358). The backend
html-document-backend.md designs is implemented: docrender.HTML
renders -doc-form html straight from the document IR, with the semantic structure, the sysml-
classes and data- model facts, the default stylesheet in a cascade layer that reader CSS
overrides without specificity fights, -html-css, -html-no-default-css, -html-default-css,
-html-fragment, and linked HTML sets sharing one sysml-document.css. The migration designed
alongside it landed in #358: -doc-form pdf hands WeasyPrint and Prince the same semantic HTML
-doc-form html writes, under a shared print.css in its own cascade layer, so -html-theme,
-html-css and -html-no-default-css reach the PDF and a relative url() or @import in a
reader's sheet resolves for the PDF engines as it does for HTML; internal/doc/docpdf's Markdown
re-parse and its own HTML writer are gone. Pandoc keeps reading the Markdown (the caption marker
it needed is a Lua filter now), and -doc-form markdown is unaffected. Prince is commercial and
not provisioned by the PDF toolchain script, so its path is exercised only on the input prepared
for it and its tests skip where the engine is absent.
The pilot as an execution referee (landed, #319, #321, #334, #413).
pilot-execution-referee.md
established that the pinned pilot evaluates model-level expressions and nothing else, so
cmd/pilot-exec-diff can adjudicate the expression rows of spec-compliance.md and no external
implementation adjudicates actions or state machines. Widening that referee means finding one,
not more harness work. For state machines the OMG PSSM suite is that referee (#230, advisory);
for actions the fUML reference implementation is, in three changes modelled on the PSSM referee
and all on develop after the tag: #319 provisions the pinned implementation and commits what
it computes over its own test models, #321 reads and classifies the activities, and #334
translates every expressible one to a fuml::<Activity> action definition by rule, runs it
under every schedule the explorer reaches and requires the values left in its output parameters
to be the reference's. #413 then widened the emitter to the constructs the first cut refused:
classes and generalizations, object creation, structural-feature actions, signals and
SendSignalAction, AcceptEventAction, active classes with their classifier behavior and
StartObjectBehaviorAction — and fixed the runtime and the SMT encoding it uncovered, where two
flows from one source pin were staged as one delivery (each flow declaration is its own
transfer now, so a fork duplicating a token down two flows delivers two values). cmd/fuml-referee
files 55 activities as 23 pass / 0 fail / 28 not-expressible / 4 differs-by-design (an
action the reference fires once per object token), pinned in
docs/project/fuml-referee-baseline.json and checked in CI by
go run -C tools ./cmd/fuml-referee -check; fuml-referee.md records the translation
rules and every row. What remains in the not-expressible bucket is recorded there: the rows
the classifier files against constructs the translation does not spell, and the emitter's own
typed refusals of an expressible activity — a ReadSelfAction in an activity performed on its
own, a start passing arguments, an edge weight other than 1, an object-flow cycle through
control nodes.
Scaling to very large models. large-model-scaling-design.md
(#300, on develop after the tag) starts from the satellite-network stress test's profiles
(satellite-network-stress-test.md, #299: about 55 µs and
8.5 KB per element to load, 16 µs per element in the workspace per keystroke) and separates the
four costs a model of millions of elements pays — per element once, per workspace per edit, per
process on one core, and per modeled object by construction — designing one approach against
each: a persistent resolver and semantic model owned by the workspace and invalidated through a
document dependency relation; closed documents held as interface records that hydrate to a tree
only when opened or queried, generalizing the standard library's snapshot and index cache;
parallel per-document analysis over a read-only index; and one definition with many occurrences
in the runtime, with sparse per-occurrence values. Each names its differential test against the
unoptimized path and the measurement that decides it. The first landed in #316: a
model.Workspace keeps its resolver and semantic model for its lifetime, records which documents
each analysis read, and drops on an edit only the edited document's frame and its dependents',
TestIncrementalEqualsFresh replaying edit sequences against a fresh workspace (a two-line edit
beside 512 satellites from 861 ms and 327 MiB to 8.7 ms and 2.0 MiB; a one-shot -validate about
a sixth slower for the recording it never uses). Open against develop on the same design: #312
(a load's files parsed, indexed and validated on a pool of workers), #309 (the REPL analyzing
each loaded file as a document of its own) and #308 (the satellite network generated as a fleet
of occurrences). Independent of every track above; the design's own sequence orders it.
An MTIP export as an optional layout augment to a SysML v1 migration (not started). A
Cameo/MagicDraw export migrates through sysml <model>.mdzip -convert sysml
(the mapping), and #524 (open, stacked on #514) writes
each of its diagrams as a view usage exposing the elements the diagram shows and rendered by
the diagram's kind. What the XMI does not carry is where the diagram draws them: the tool keeps
its diagram geometry outside the XMI, so the migrated views are auto-laid-out like any other
and the arrangement an engineer settled on is lost. Open-MBEE's MTIP plugin
(Open-MBEE/mtip-cameo) reads that geometry through the tool's own API and writes it into its
HUDS XML — per diagram, the shown elements with their bounds, connector breakpoints, colors,
fonts and images, each keyed by the element's identifier — so an MTIP export of the same
project holds exactly the layer the migration lacks. The proposal is to read it as an
augment, not a second input format: the .mdzip (or .xmi) stays the one source of the
model's structure and behavior, and an optional -layout <mtip-export.xml> supplies presentation
only. The reader takes the diagram/presentation layer of the HUDS file and nothing else, joins
each record to the migrated view and the exposed element or connector by the element identifier
MTIP records. MTIP has an export setting for that identifier (local, its default, or cloud);
the join needs the local form, which the design must first confirm on a real export is the
xmi:id the migration already resolves. The reader then writes
the geometry the way diagram-layout-annotations.md already
carries it — metadata Layout about … { x; y; width; height; } and metadata Route about … { points = (…); } in the view usage's body, with @Canvas for the diagram's extent — so the
result is standard metadata every conforming tool preserves and every OpenSysML rendering
honors, not a sidecar and not comments. What DiagramLayout has no attribute for (colors,
fonts, images) is reported and dropped rather than invented into the library; extending
DiagramLayout for it is a separate decision. A record whose identifier matches no migrated
element, a diagram MTIP exports that the migration skipped, and a malformed or partial record
are each a named row in the migration report, never silently lost; and a -layout file written
against a different project than the model refuses with the mismatch stated rather than
annotating nothing. Without -layout the output is byte-identical to today's. Generic by
construction: the HUDS schema is MTIP's, not any one project's, and the fixtures are MTIP's own
test exports plus a small hand-written pair (model + HUDS) with a validated golden; the proof
on a real project needs an MTIP export of a model whose .mdzip is also at hand, and no such
pair exists yet — that file is the gate, not the code. Builds on #524's view usages and the
landed DiagramLayout library; independent of every track.
The module is one Go module of 72 packages under internal/, 49 of them under internal/core/.
The import graph is acyclic, as Go requires, but it is not layered: the runtime imports the
validation suite and the parser, the RDF exporter imports the runtime, the REPL imports the gRPC
service, and test-only and referee packages sit beside the product packages with nothing telling
them apart. Nothing here changes what any binary does; the track is the separation of the
parser, the semantic engine, the runtime and the translation utilities into layers that can each
be built, tested and reasoned about without the layers above them, whether or not they ever ship
separately. It was deliberately not started before the 0.9.0 cycle: the moves touch files
every open pull request touches, so the track waited for the pull requests open against
develop at this baseline to land, with the small items first and the two large ones last; the
tooling module (P3) and the small moves (P1) are in, the tests tree (P2) is scheduled. The
figures below are the baseline the track started from. Measured at develop 530c04667
(#354), non-test lines and go list import edges; the
binary, test and directory figures below at develop 206760826 (#384), where the module has
70 packages under internal/.
Transitive dependencies on other packages of the module: internal/syntax/parser 4 (source,
lexer, ast, quickfix) — the parser is separable today; semantics 9; lower 10;
passes 19; runtime 22, including parser and all of passes; export 29, including
runtime, lower, migrate, parser and libs; model 32. internal/exec/runtime is
71,520 non-test lines, 29% of the module's non-test code, and 84 of its 138 non-test files import
internal/syntax/ast.
The edges that break the layering, each with the files that carry it:
- Runtime → validation.
internal/exec/runtimeimportsinternal/check/passesin seven files, for two things:passes.Diagnosticandpasses.Severity*as the runtime's own diagnostic type (choice.go,tool.go,modeled.go), and the invocation selection inpasses/invocation.go—SelectInvocation,ChainCallee,NewArgumentTyper,InvocationArgs— which both the type checker and the evaluator need (eval.go,holders.go,binding_reads.go,model.go). Becausepassesitself importsdocplan,queryplan,view,rdf,identityandlower, executing a model links document planning, diagram layout and the RDF vocabulary. - Runtime → parser.
runtime/tool.gocallsparser.Newto parse text at execution time; the runtime should receive a tree, not build one. - Translation → execution.
internal/translate/exportimportsruntimeandloweringraphs.go,graphs_action.goandgraphs_state.gofor thegraphs:1form, andmigrateandparserinconvert.gobecausesysml -convertwas implemented inside the exporter. In the other directioninternal/exec/analysisimportsexport(engine_entry.go,external_question.go) for the same form, so execution and translation depend on each other through two packages. An RDF exporter cannot be built without the runtime. - Validation → execution IR.
passesimportslower(send_action.go,action_endpoint.go,state_transition.go,w8d_assignment_referent.go,typecheck.go). architecture.md describesloweras "AST → execution IR for the runtime"; validation consuming it means it is a semantic IR shared by both, and the layering should say so rather than place it under the runtime alone. - Frontends sideways.
internal/frontend/replimportsinternal/frontend/grpcforInstanceGraphToProtoandGraphBounds(repl/features.go), so the terminal REPL links the Connect service layer;cmd/sysmlimports nineteeninternalpackages directly rather than throughreplormodel;internal/frontend/lspimportsinternal/translate/interop/reposync. - Type system → scanner.
internal/semantic/semanticsimportsinternal/syntax/lexerfor its notation-text helpers (NameText,UnrestrictedNameText,StringValue,CommentBody,IsIdentifier,IsKeyword) indocumentation.go,units.go,unit_product.go,annotations.goandfilter.go.
What is in internal/ that is not product code, or is in the wrong place:
internal/hygieneandinternal/perfbenchhave no non-test files; they exist to hostTestNoProductionCodeImportsTestingand the benchmarks.internal/baseline,internal/fixtures,internal/junit,internal/doccounts,internal/stressmodel,internal/fuml,internal/pssmandinternal/testutilare reached only fromcmd/pilot-*,cmd/pssm-referee,cmd/fuml-referee,cmd/stress-model,cmd/doc-countsandcmd/validation-census, never fromsysml,sysml-lsporsysml-grpc.internal/erratais both:core/libs/source.goapplies its overlay to the bundled standard library (product), and the pilot tools read its registry (tooling). It can only move once the overlay is split from the oracle bookkeeping.internal/xmi(the XMI 2.5 reader of the UML test suites, forfumlandpssm) andinternal/translate/xmi(the XMI reader of SysML v1 exports, formigrate) read the same format into two element trees.internal/workspace/envvarandinternal/workspace/projectare process configuration, not language core;internal/doc/docpdfis the PDF backend of thecore/docrenderpipeline and the only part of it outsidecore/.
sysml built by make build on linux/amd64 is 50.0 MB (sysml-grpc 49.0 MB, sysml-lsp
29.0 MB). Of its symbol bytes 66% are internal/* code, 15.5% are gRPC-go, Connect, protobuf,
net/http and crypto/tls, and the rest is the Go runtime and standard library; the embedded
standard library is 5.4 MB of data (the 1.8 MB source tree and the 3.6 MB parsed snapshot).
Three things follow:
- Test code is not in the binaries. Go compiles no
_test.gofile into a non-test build, andgo list -depsshows none ofhygiene,perfbench,testutil/*,fixtures,doccounts/doccountstest,junitorbaselinelinked intosysml,sysml-lsporsysml-grpc. Moving tests out ofinternal/tidies the tree; it changes no binary. - Debug data was. The Makefile's
LDFLAGSstamped versions and nothing else, so the binaries shipped their symbol table and DWARF;-s -wdrops both and takessysmlto 37.2 MB with the version stamps, build info and stack traces intact. That is a build-flag change, made outside this track. - The service edge cost almost nothing. A probe linking every
internalpackagesysmlreaches exceptreplandgrpcis 17.6 MB and the same probe plusinternal/frontend/grpcis 30.3 MB, which suggested therepl → grpcedge above cost a quarter of the terminal binary. Removing it did not: the linker was already dropping the unreachable service, and the unstrippedsysmlondevelopcarried 450 KB of gRPC-go and 2.6 KB of Connect symbols. Moving the conversion tointernal/frontend/protoconvtooksysmlfrom 37,224,632 to 37,220,536 bytes and its transitive gRPC-go and Connect packages from 53 to 51: Connect andinternal/frontend/grpcare gone, and the 51 are gRPC-go, reached throughapi/protoitself, because the generatedsysml_grpc.pb.goshares the Go package with the message types. Taking the count to 0 is a codegen change — the service stubs generated into a Go package of their own, with ago_packageof its own and every generated client updated — and is the follow-up of P1, not a move.net/httpstays becauseinterop/flexoandinterop/reposyncare HTTP clients.
346k lines of the module are tests, against 304k lines of product, in 1,289 _test.go files;
1,207 of them declare the package they test (white-box, reaching unexported identifiers) and 82
declare an external _test package. 114 test files walk a testdata directory; the 22
testdata directories under internal/ and cmd/ hold 19 MB, 11 MB of it the runtime's
conformance and trace fixtures, and add 32 directories to the tree. Go requires a white-box test
to sit in its package's directory, so the unit tests stay where they are; what can move is the
black-box suites, every testdata tree, the two test-only packages and the four test-support
packages.
Identically named unexported helpers defined in four or more non-test packages at the baseline, each a copy, and where each lives now (P4):
| helper | copies | where | home now |
|---|---|---|---|
qualifiedNameText(*ast.QualifiedName) string |
4 | docplan, lower, runtime, symbols |
ast.QualifiedName.Text |
ownerOf(*symbols.Symbol) *symbols.Symbol |
6 | codegen, edit, passes, resolve, semantics, view |
symbols.Symbol.Owner |
declMembers |
4 | lower, passes, runtime, semantics |
ast.DeclMembers; see below |
lastSegment, qualifiedName |
5 each | export, queryexec, runtime, symbols, repl; export, migrate, queryplan, rename, solve |
symbols.LastSegment, ast.QualifiedNameOf; see below |
sortedKeys |
6 | across the module | slices.Sorted(maps.Keys(m)) |
moduleRoot, writeReports, classify |
5, 4, 5 | cmd/pilot-diff, pilot-reject, pilot-xpect, grammar-coverage, validation-census |
tools/oracle/repo, tools/oracle/report (P3) |
The name-sharing was wider than the copying. Of the declMembers four, three were the same
function (a definition's or usage's Members) and are ast.DeclMembers; the runtime's also
walked owned-constraint bodies and unwrapped Membership, so it stays as
runtime.unwrappedDeclMembers, and semantics' takes a symbol and adds AssumeMember bodies,
so it stays too. Of the lastSegment five, symbols and suggest split a :: name and are
symbols.LastSegment; runtime's splits a dotted feature path, queryexec's an object label
at . or :: outside quotes, repl's returns four values for completion, and export's
takes a string — different functions under one name, kept. Of the qualifiedName five,
solve's built an *ast.QualifiedName from segments and is ast.QualifiedNameOf,
queryplan's and rename's rendered one and are QualifiedName.Text, migrate's reads an
XMI element, and export's stays until the exporter's own track touches graphs.go.
The larger reuse gaps are the ones the edges above already name: two XMI readers, calc lowering
in runtime rather than lower, invocation selection in passes reused by runtime, and the
REPL reaching into grpc for a conversion.
The instance model is clean: runtime.Instance is an id, a *symbols.Symbol and a map of
FeatureValues over EffectiveFeatures, Value wraps semantics.Value, instance ids,
collections and symbols, and instance.go, value.go and features.go together name ast.
24 times, all for the expression payload of a ValExpr or function value. There is no
execution-owned layer between it and the syntax tree, though: the lowered graphs in
internal/ir/lower are side tables keyed by AST nodes (ActionGraph.Nodes []ast.Node,
Edges map[ast.Node][]ActionEdge, StateGraph.Behaviors map[*ast.StateNode]*StateBehaviors,
Transition{Source, Target, Trigger, Guard ast.Node}), so a node's identity is its AST pointer
and state_executor.go alone names ast.StateNode 211 times; expressions are never lowered,
runtime/eval.go interpreting ast.OperatorExpr, ast.InvocationExpr and ast.FeatureChainExpr
directly and compile.go compiling the same tree into closures held in a side table; and calc
lowering (calcShape and its Steps) lives in runtime/invoke_calc.go, not in lower. This
is the immutable-AST, side-table design AGENTS.md §4 requires and is not a defect, but it is
why the runtime cannot be exercised without real trees, why a change to an AST node type reaches
lower, passes, runtime and export/graphs_* at once, and why a compiled tier would have
to introduce the missing layer first.
A package imports only the layers below it:
| layer | packages |
|---|---|
| foundation | source (with the notation-text helpers and ReplaceFile), ast, ast/astcodec, pack, diag (with the quick fixes and ConformanceMode) |
| syntax | lexer, parser, format |
| semantics | symbols (with Origin), suggest, resolve, semantics (with invocation selection), identity (with the normative ids) |
| semantic IR | lower, queryplan, docplan |
| validation | passes (the root registers; passes/kit, passes/behavior, passes/document, passes/diagram, passes/identity), edit (with the rename conflict check) |
| execution | runtime, solve, smt, analysis, engines, objref, the graphs:1 form |
| translation | rdf, export, migrate, simresults (the migration's result sidecar), convert (the conversion entry point), one xmi, codegen, interop/* |
| documents | queryexec, docir, docrender, docpdf |
| workspace | model, libs, project, envvar |
| frontends | protoconv, repl, lsp, grpc, stdiorpc, usage, cmd/* |
| tooling | baseline, the errata registry, junit, doccounts, fuml, pssm under tools/; fixtures, stressmodel, perf, hygiene, testutil under tests/; never linked by a shipped binary |
The tree says the same thing as the table. internal/ today is 121 directories: 70 packages,
32 testdata subtrees, the 17 data directories of the bundled standard library and two grouping
directories, with 21 entries at the top and 41 under core/. The target is nine directories under
internal/, one per layer — syntax, semantic, ir, check, exec, translate, doc,
workspace, frontend — holding about 45 packages; a tests/ tree at the repository root for
the black-box suites and every fixture; and a tools/ tree, a nested Go module of the same
repository, for the eleven cmd/ programs that are never released (conformance, doc-counts,
fuml-referee, grammar-coverage, pilot-diff, pilot-exec-diff, pilot-reject,
pilot-xpect, pssm-referee, stress-model, validation-census) and the ten packages only they
link. The product go.mod then lists the product's dependencies and go build ./... at the root
builds the product.
A layering test beside TestNoProductionCodeImportsTesting — a table of layer → permitted layers
checked against go list -f '{{.Imports}}' — pins each edge as it is removed; make lint is
staticcheck and gosec and checks no import boundary today.
Each landed as a pull request of its own, cut from develop:
semantics → lexer: the notation-text helpers (NameText,UnrestrictedNameText,StringValue,CommentBody,IsIdentifier,IsKeyword) live insource;lexerandsemanticsboth import them from there.runtime → passesfor diagnostics:DiagnosticandSeveritylive ininternal/syntax/diag, a leaf packagepassesandruntimeboth import.runtime → passesfor invocation selection: the runtime selects throughsemantics.Model.SelectCall, andChainCalleeandInvocationArgslive insemantics. The checker's argument typing stays inpasses— it is the whole expression checker — and the code that builds a model for execution installs it withSetArgumentTyper; a runtime that reaches selection with no typer installed returnsruntime.ErrNoArgumentTyperrather than selecting with weaker typing than validation used, and a hygiene test checks every product construction site installs it.runtime → parser: the runtime parses notation text through anExpressionParserthe model's builder installs withSetExpressionParser, and returnsErrNoExpressionParserwithout one.go list -deps ./internal/exec/runtimenames neitherpassesnorparser.repl → grpc: the value and instance-graph conversion lives ininternal/frontend/protoconv, which depends onapi/proto,internal/*and the protobuf runtime;repl,grpcand the Go client import it, andsysmlno longer linksinternal/frontend/grpcor Connect. The gRPC-go packages still linked, and the codegen follow-up that removes them, are measured under "What the binaries carry" above.export → migrate, parser: the conversion entry point isinternal/translate/convert— the formats,Convert,ConvertTolerant,SysMLElement,Migrate,SysMLToRDFandSyntaxError— andcmd/sysml,repl,grpcandinterop/flexocall it.exportkeepsToRDFandToSysMLand no longer importsmigrate; it still importsparser, because the graph → notation decoder parses by nature (it re-parses preserved source text to check it still encodes to the graph, parses an expression to judge its binding, and parses names), and translation sits above syntax in the table.
The layering test beside TestNoProductionCodeImportsTesting pins the layer table and the
removed edges, alongside per-edge tests of the runtime's parser and typer seams, protoconv's
dependencies and the conversion entry point's ownership of migration.
tests/ at the repository root holds the black-box suites, one package per suite, each with its
fixtures beside it (#392, #397, #395, #401): TestNoProductionCodeImportsTesting (tests/hygiene,
where the layering test goes too); the benchmarks (tests/perf); the parser's golden ASTs and
negative cases (tests/parser); the four OMG corpus gates and the RDF round-trip ratchet
(tests/corpus); the gRPC conformance suite (tests/grpc); 81 of the 82 external test files, by
package (tests/export, resolve, semantics, migrate, reposync, suggest, identity,
model, queryplan, ontology); the top-level testdata/ as tests/testdata; and gobuild and
graphcmp as tests/testutil. The drivers that reached into their package's unexported identifiers
were rewritten against the exported surface — the gRPC suite against grpc.NewService with its own
unit-term formatter, the export suite with its own pilot-corpus loader — not given an export shim.
The corpus policies did not move: the training corpus is an assertion, the pilot roots and the round
trip per-file ratchets, and the download scripts and require-variables are as AGENTS.md §2 states
them. Measured at the track's baseline 206760826 and after the four pull requests: testdata
subtrees under internal/ 32 → 22, packages 70 → 66, entries at the top 21 → 18 (hygiene,
perfbench and testutil gone), external test files beside a product package 82 → 1; 34
directories and 17 packages under tests/. With P3's moves the tree under internal/ is 121
directories → 97.
What stayed, and why. The runtime's execution conformance and trace suites
(TestExecutionConformance, TestExecutionTrace, conformance_test.go, trace_test.go and the 11
MB under internal/exec/runtime/testdata) are white-box in fact, not only by location: sixteen
runtime-internal test files run the same cases through the driver's schema types, loaders and case
runner, eleven of them reaching unexported Context state (snapshots, held images, replay, the
explore queue), and about 1,700 of the 2,300 lines of conformance_test.go depend on the runtime. A
package runtime test cannot import a package that imports internal/exec/runtime — Go rejects it
as an import cycle in test — so a shared harness under tests/ cannot be reached from those files,
and a copy of the runner in each tree is the duplication this track removes. The driver and its
fixtures stay beside the runtime, and the census reads them there. The LSP and REPL suites stay too:
the files that touch no unexported identifier still share package-local helpers with the white-box
ones (mustDebug, render and openRenameDoc in lsp; meta, submitModel and evalOK in
repl), and moving them would copy those rather than share them;
internal/frontend/grpc/oslc_query_repl_test.go is the one external file left, reaching mustNewService and
queryModel through export_test.go. And the thirteen testdata trees still under internal/
belong to white-box drivers, so they sit beside them — a fixture moves with its driver, not on its
own. Of the support packages the plan named, doccountstest went to tools/census/doccounts
with its owner, as P3 records, and fixtures to tests/fixtures under P4.
The eleven unreleased cmd/ programs and the packages only they link — baseline, junit,
fuml, pssm, xmi, doccounts, core/libs/gensnapshot, core/rdf/ontology/gen — are in
tools/, a nested Go module of the repository (tools/go.mod, replace … => ../), laid out
by what each tool does rather than one directory per OMG artefact, which is how fuml, pssm,
xmi, baseline and junit came to sit as five siblings of the compiler (#394, #400, #404):
tools/
referee/ fuml pssm xpect diff reject exec the oracles, one package each
oracle/ xmi baseline junit errata report repo what every referee shares
census/ validation grammar doccounts the counting gates
gen/ snapshot ontology generators
cmd/ one main per program
Every program runs as go run -C tools ./cmd/<name>, and because -C starts it inside the
tools module, a relative path given to any of its flags (-out, -baseline, -junit, …) counts
from the repository root, which repo.Resolve applies; report took writeReports and the
four verdict buckets the fUML and PSSM referees share, repo took moduleRoot, and the copies
are gone. errata is split: the overlay the standard library applies is
internal/workspace/libs/errata (product), the registry the oracles read is tools/oracle/errata.
There is no go.work: go build ./... and go test ./... at the root are product-only, and
make test and make lint run both modules. Two of the listed packages could not follow the
tools into the nested module, because product tests import them and a test cannot import the
nested module without a cyclic requirement: fixtures, which also took the analysis-library
census schema that the runtime's own test writes and tools/census/doccounts counts, and
stressmodel, whose generator internal/workspace/model's incremental test drives (only
cmd/stress-model moved). They stayed under internal/ at first; P4 moved them to tests/,
which both the root module's tests and the tools module can import.
docpdf is internal/doc/docpdf, beside docrender; envvar and project already sat beside
model, and whether they fold into it is P4's question. Measured at develop 1e44c746c
before and after the four pull requests: go list -deps ./cmd/sysml 368 → 368 (nothing moved
was on a shipped binary's path, and go list -deps of the three binaries names no package of
tools/); directories under internal/ 119 → 110, 21 → 13 entries at the top and 70 → 61
packages. The validation-pass split is recorded below, with registration left central.
passes is split by domain, in five pull requests stacked bottom-up. passes/kit is the
framework every check is written against — Pass, PassLevel, Context, Options, Gathers
and the symbol and member walkers — a leaf the root re-exports as type aliases so passes.Context
and passes.NewContext read as before. passes/document holds the document-plan and
document-query checks, passes/diagram the layout and view-rendering checks, passes/identity
the identity-metadata check with the workspace-wide identity gather, and passes/behavior the
state-transition, succession-endpoint and control-node checks. Registration is unchanged and
stays central: passes.DefaultRegistry is the one place a check is registered, in the same order
as before, so the root imports every domain package and the layering test pins that no domain
package imports the root. The remainder of the root is the core: the registry, the static
expression typer and the checks that call into it — among them the three behavior checks
send_action.go, transition_guard.go and typecheck_trigger.go, which follow the typer when it
moves — the constraint checker and the structural rules, and the OOSEM and MOSA audits. Every
diagnostic code, message, range and quick fix is byte-identical; the corpus gates, the golden
tests and the validation census are the assertion, and statement coverage over the tree is unchanged. Measured before and after the five pull requests:
go list -deps ./cmd/sysml 363 → 368, go list ./internal/... 54 → 59 packages, directories
under internal/ 95 → 100 — the five new packages and nothing else; 82 non-test files in one
package become 71 in the root, 5 in kit, 4 in behavior and 3 in each of document, diagram
and identity.
The helpers first, one pull request per family, each deleting every copy and routing the
callers to one home: ast.QualifiedName.Text and ast.DeclMembers (#411);
symbols.Symbol.Owner (#415, which also retired the fqnOf, elementID and contentName
wrappers of symbols.FQNOf whose fallback could not fire); symbols.LastSegment and
ast.QualifiedNameOf (#418, on #415); and slices.Sorted(maps.Keys(m)) for the sortedKeys
copies of passes, symbols, interop/flexo and interop/reposync (#417, cut from
develop on its own). The table under "What is duplicated" records which same-named functions
were not copies and stayed.
Then the one-file packages, each into the package that owns the concept, with the layering
table consulted first so no fold adds an edge: quickfix into diag as diag.Fix, Edit,
Replace and InsertLine (#419) — both are foundation, and every importer of one already
imported the other; conformance into diag as diag.ConformanceMode, ConformanceDefault,
ConformanceStrict, ConformanceModeOf and ParseConformanceMode (#430), the mode being the
switch that decides a finding's severity; fsutil into source as source.ReplaceFile
(#431), the one filesystem write the product does atomically; identity/normative into
identity as normative.go (#420), which was its only importer; provenance into symbols as
symbols.Origin, Symbol.Origin, NodeOrigin and OriginAt (#426), the symbol table being
where a declaration's location already lives; and rename into edit as edit.CheckRename,
RenameConflict and RenameOccurrence (#422), beside the layout edits that apply a rename.
Where a fold moved a self-model unit, examples/self-model/surfaces.sysml names the new
package, and the pilot differential baseline records the examples digest that follows.
The test-support packages P3 could not take into the tools module moved to tests/ (#427):
internal/fixtures is tests/fixtures and internal/stressmodel is tests/stressmodel, both
still in the root module, so tests/model, the census in tools/census/doccounts and
tools/cmd/stress-model import them from there. go list -deps of sysml, sysml-lsp and
sysml-grpc names neither before nor after, and the layering test pins that no production
package imports anything under tests/.
Three of the folds the plan named did not happen, each for a reason the import graph gives:
engines → analysisis a cycle.enginesimportsanalysisandsmt, andsmtimportsanalysis; folding the registry intoanalysiswould makeanalysisimportsmt, which imports it.enginesstays as the leaf that links the engines to the framework.envvar → modelis a cycle too:runtimeimportsenvvar(the tolerated edge the layering test lists), andmodelimportsruntime.project → modelwas left on ownership:modelis the workspace, with no filesystem or input-discovery surface, andprojectis the command line's input discovery, used bycmd/sysmlandreplonly. Both stay besidemodelin the workspace layer.enginewire → analysisis not a package move. Eight of the wire's types —Question,Model,Result,Budget,Bound,Witness,Input,Description— carry the names of the framework's own types inanalysis, deliberately: the wire's vocabulary is the external engine protocol's, not the framework's, and it is what the schema test and the stand-in engine undertestdata/speak. Folding would rename the protocol to fit beside the framework, soanalysis/enginewirestays a package of its own, and the layering test keeps its row and itsexportedge.
Measured at develop b270ebbc9 before and with the eleven pull requests applied:
go list -deps ./cmd/sysml 367 → 361; packages under internal/ 60 → 52; directories under
internal/ 100 → 92, entries at the top 11 → 8 (fsutil, fixtures and stressmodel gone)
and under core/ 44 → 40. The remaining distance to the target's 45 is the three packages
above and passes split by domain, which P3 records as still to do.
The pull requests land in this order, each cut from the one before it except where said:
#411 → #415 → #418 → #419 → #430 → #431 → #420 → #426 → #427 → #422, and then this
document's update; #417 is cut from develop and merges anywhere in the sequence.
Three pull requests, each stacked on the one before:
analysis → export: thegraphs:1andsourcesforms an external engine receives —GraphsVersion,Graphs,GraphsOf,MarshalGraphs,Sources,SourcesOfand their refusals — live ininternal/exec/analysis/modelform, in the execution layer, with a golden per graph kind pinning the emitted bytes.exportkeepsToRDFandToSysMLand names no runtime type:go list -deps ./internal/translate/export(141 → 135 packages) lists neitherruntimenorlower, so the audit ofgraphs_*.gohad nothing left to move.- One XMI reader:
internal/translate/xmiis the generic XMI 2.5 element tree (moved from the tooling module, where the fUML and PSSM referees still import it), andinternal/translate/xmi/sysmlv1is the SysML v1 interpretationmigratereads — stereotype applications, href proxies, MagicDraw archives — as a walk over that tree rather than a second parser. The one reader rejects anxmi:iddeclared twice, as the referees' reader always had. - The layering test pins
analysis → export,export → runtimeandexport → loweras removed edges.
Every package under internal/ sits in one of the nine layer directories of the target —
syntax, semantic, ir, check, exec, translate, doc, workspace, frontend — and
the layering test names every layer with the layers it may import; internal/core/ is gone.
The item was written above as one pull request; it landed as a stack of nine, one directory per
pull request in the table's order (#440 syntax, #441 semantic, #442 ir, #443 check, #444 exec,
#445 translate, #446 doc, #447 workspace, #448 frontend), each based on the one before it and
rewriting only the import paths its own move forces, root module and tools/ module alike,
followed by this documentation pull request. Every package keeps its last path element; nothing
was renamed, merged or split, and no behavior changed. Three packages the target table did not
name are placed by their imports: highlight and query under semantic, view under ir;
analysis/enginewire stays nested under exec/analysis. The one edit beyond paths is
semantic/query, which declares the three namespace IRIs its OSLC prefix map needs rather than
importing translate/rdf, so the semantic layer no longer reaches translation; the layering test
pins that edge as removed. The edges the test tolerates are the seven develop carried into the
stack, path-rewritten, and exec/runtime still receives its parser and argument typer from the
caller.
Measured at refactor/layer-frontend e0ecd2069 against develop 659391162:
go list ./internal/... 59 packages before and after; internal/ is 108 directories
(was 100); go list -deps ./cmd/sysml 368 before and after; 9006 Test
functions and packages reporting ok before and after; no shipped binary links tests/,
stressmodel or fixtures. The examples digest of the pilot differential and the errata
registry digest of the pilot baselines moved because the self-model's goPackage strings and
libs/errata's LibraryRoot embed package paths; no count moved.
Give lower's graphs their own node identities (an opaque id with the originating ast.Node
kept for diagnostics), lower expressions once rather than interpreting the tree in two
evaluators, and move calcShape lowering from runtime into lower. This is the item that
creates a layer between the syntax tree and the instances; it is feature-sized work under
AGENTS.md §8 and goes last, when the graph the layering test guards is otherwise clean.
Two orders, because there are two kinds of item. The track-local orders say where to start
inside a track; the cross-cutting order says which tracks' first items go first when a session
must choose. The order agreed at the v0.6.0 baseline had F and S first and A6/X6/A2 second, and
owed X2's chain-read half; all of it landed and v0.7.0 and v0.8.0 shipped it, so both orders
below are rewritten again around what remains. Of the 215 pull requests merged to develop
after the tag, these move a roadmap item and are named where they do: #267, #289 and #293 (Q2,
now closed), #359 (Q1's page and Q3, closing Track Q), #263 (A7), #286 and #364 (the release
fold-backs), #291 (the generated test figures), #292 (L7, closing Track L), #296 (A4, closing
Track A), #335 and #352 (E1), #466 and #479 (a braced state block as one action), #362 (E2),
#465 (E4), #468 (E7's dynamic objects), #344 (modeled randomness beside A3), #356 (W3's
rasterization and export), #358 (the PDF path onto the HTML backend), #300 and #316 (the
large-model design and its first step), #319, #321, #334 and #413 (the fUML referee for actions
and its emitter), #350 (R4's installer wizard), #304 (the errata overlay, under "Upstream
follow-through") and the state-executor fixes and findings the PSSM referee adjudicated,
#384, #438 and #513 (the region-order choice points) among them (Track E). Open against
develop and touching an item: #308, #309 and #312 (the large-model design's next steps), #531
(the SysML v1 migration documented across the API, the guide and D13, superseding #327) and #525 (Terminate
002's do step against a sibling region's entry units). release/0.8.1 (#347) is tagged
v0.8.1 and folded back.
Everything the previous baseline listed as unreleased is in a tag, and the two releases since then carried more than that list. By track, with the pull requests the tracks cite:
- Track F — closed: F1 and F2 (#116), F3 (#120);
known_failures.txtis empty. #119 moved the synchronized step boundary the debugger stops at. - Track S — landed: S1 (#110), S2 (#123), S3 (#125), S4 (#134); #141 made region order a choice point, #138 wrote the guide.
- Track X — landed: X2 in full (#164 closed the chain-read half), X3 (#115), X4 (#113), X5
(#211, #238, #239), X6 (#122), X7's values (#121), X8's typing (#112); the
metacast (#212) and static expression typing landed beside them. - Track A — landed: A6 (#117), A3 (#118), A2 (#133), A5 (#136); A7 (#263) and A4 (#296) are
on
developafter the tag, and the track is closed; #344 added modeled randomness and Monte Carlo runs beside A3 there too. - Track Q — landed: Q4 (#849), Q2's expression half (X5); Q2's query side (#267, #289, #293)
is on
developafter the tag and closes Q2. - Track L — landed: L3–L6; L7's measured table (#292) is on
developafter the tag and closes the track. - Track W — landed: W1 (
dot), W2 (plantuml) and W3's plumbing for both; W3's PDF rasterization of both forms and the panel's export of them are ondevelopafter the tag (#356). The VS Code diagram panel also grew there without touching a form item: it runs the behavior it draws through the language server'sopensysml/debug/*requests (#294), opens a document's several views (#349) and opens on demand (#348), writes layout into the document that declares the element across the workspace (#307), and reparents by drag (#305). - Track E — complete: E1 landed; E2 landed; E3 closed by record; E4 landed; E5 closed by
record; E6 landed; E7 landed; E8 landed; E9 landed; E10 landed. E3's record
(expansion-regions.md) closes the parallel per-element form because the
iterative form is
for; E5's record closes protocol state machines because they are not a SysML v2 construct; the exception-handlers record (exception-handlers.md) closes the compliance mapping's last UML-referenced action item the same way, leaving its Implementable But Not Yet Done list empty. - Track D — D1, D2, D7, D9 (branch read and whole-graph push), D11 and D12 are done. D3's Flexo harness is measured at 505 of 582 properties delivered. D8's ontology profile and D10's write-through remain open; D13 (the SysML v1 migration's remaining items) is written.
- Release follow-through — R4's Windows installer is published by
v0.7.0andv0.8.0alike; the release procedure runs git-flow (#151);opensysml0.5.0 is on PyPI; the test-suite figures are generated and gated (#291, ondevelopafter the tag).
The open items, by track, with the item that gates each where one does. Everything not named here is landed or is a track the previous baseline left as it stands (D, N, M, I, V, B, R2–R5); Tracks F, S, L and A are closed.
- Track E — complete: E1 landed; E2 landed; E3 closed by record; E4 landed; E5 closed by
record; E6 landed; E7 landed; E8 landed; E9 landed; E10 landed; exception handlers closed by
record. Optional runtime follow-ups are not SysML v2 specification gaps. The
PSSM referee's 13
failtests are the state side's measurement, every one attributed (pssm-referee.md): five cite a differs, v2 silent alignment row; three (Entering 010, Entering 011, Junction 005) a translation limit of the referee's; four (Transition 017, History 001-C, History 002-B, Exiting 002) a defect of the suite recorded in omg-issues.md; and Terminate 002 the runtime's one open gap in the region-order work — a duedostep is not drawn against a sibling region's remaining entry units — which #525 (open) addresses. - Track Q — complete: Q1 is written, Q2 and Q4 are done, and Q3 landed in #359 on
developafter the tag. - Track X — X7's native layout for sets and tensors; X8's two harness halves (pilot-differential numeric normalization with an adjudication file, and a standalone RDF expression-tree round trip).
- Track W — richer DOT/PlantUML node shapes and compartments; W3's rasterization and export landed in #356.
- Release follow-through — R2, R3, R5 (account- and hardware-gated), and nothing else: the
v0.8.0post-tag checks are met and the test figures are generated. - Tracks N, M, I, V, B — as the tracks state them; nothing in them moved since
v0.6.0except the fourValuearms Track I's clients carry (#113, #121, #122). Track D's current status is stated above. - Proposed — scaling to very large models (the design and its first step landed, #300 and
#316; #308, #309, #312 open). The PDF path onto the HTML backend (#358) and the fUML referee
for actions with its emitter (#319, #321, #334, #413; the
not-expressiblerows left are typed refusals recorded in fuml-referee.md) are landed; the order of orthogonal regions is a recorded choice point (#384, #438, #513), with Terminate 002's one trace left to #525. An MTIP export as an optional-layoutaugment to a SysML v1 migration, writing the tool's diagram geometry into the migrated views asDiagramLayoutmetadata, is not started and waits on an MTIP export of a model whose.mdzipis also at hand.
The release housekeeping the previous order opened with is done — #286 folded main back into
develop, PyPI serves the Python client's 0.5.0 — and its steps 2 (L7, #292), 3 (D2 and D1,
#509), 4's first half (Q2, #293) and 5 (A4, #296) landed on develop, so the order is shorter
by four.
Track E — complete: E1, E2, E4, E6, E7, E8, E9 and E10 landed; E3 and E5 closed by design record. Its remaining design notes do not add an open Track E item.
Track Q — complete: Q3 (#359) landed on develop after the tag, on A5's clock and Q2's
object rows; Q1, the page that says which query is which, is written in the document-generation
manual; #267, #289 and #293 closed Q2; Q4 (#849) landed independently ahead of them.
- I2, I3, then I4's client — the shared fixtures, the thin R, Julia and MATLAB packages, the C client, each derived from the wire contract (I1, landed in #848); the C ABI half of I4 is not here — it is step 7.
D2 and D1 — Flexo: the standard vocabulary for expression trees and end structure (the collection JSON annotations, D3.4, landed in #850; the branch read and the authenticated whole-graph push, D9.1 and D9.2, landed withLanded in #509; the live-stack harness was re-recorded at 505/582 properties, so the order is shorter by one more step.-convertover a branch URL).- X8's harness halves, then X7's native layout — normalization and adjudication in the pilot differential and a standalone RDF expression-tree round trip, so every later expression item is measured; the set and tensor native layouts when something needs them.
- B1, then B2 — the binding vocabulary, then the provider contract in the runtime and Go API. The two items depend on nothing outstanding (the wire contract is landed, the dispatch and materialization seams exist) and touch only the metadata library, one pass and the runtime's dispatch, so they can run beside steps 3 and 4; B2 drains events into loops that carry the clock (A5) and the choice points (S2). B3 follows the transport decision, I5 goes with step 3 since it is built from the same fixtures, and B4/B5 come whenever a model needs them.
- M1, then M2 (with N2.6) — the closed behavior IR, then the state and action C backend over static tables. Embedded behavior compilation depends on the closed IR; the native track's action and state phases start from the same IR rather than a second one. M3 and M5 prove it; M6 last.
- The shared C ABI — N2.4 / I4 / M4 designed once, after N2.2 (the budget) is decided and after M1 fixes what an embedded entry point looks like, so a stable native/embedded calling contract exists to design against rather than three.
Beside the order, whenever a session has room: Track V's census rows (53 unknown), Track W's
remaining writer work, and the large-model design's next steps in its own sequence (#308, #309
and #312 are the open ones). One release is behind and one in view. release/0.8.1 (#347) was
a patch cut from v0.8.0 by cherry-pick, carrying the bug fixes since the tag and the three VS
Code extension fixes, and deliberately none of the state-executor series or the features; it is
tagged v0.8.1, moved no roadmap item, and #364 folded main back into develop as after
v0.8.0. The next cut from develop carries everything else that landed since the tag — Q1,
Q2 and Q3 closing Track Q, L7, A4, E1 and E2, E4's streaming flows, E7's dynamic objects, the
generated figures, modeled randomness, the fUML referee and its emitter, the region-order
choice points and the state-executor fixes, W3's rasterization and export, the PDF path onto
the HTML backend and the workspace's persistent semantic model — and by CONTRIBUTING.md §
Versioning it bumps the minor segment, not the patch: features are patch material there, but
#302 refuses a construct v0.8.0 accepted (a body inside a nested definition reaching the
enclosing definition's features by their bare names, now Must be an accessible feature), the
completion, junction, join and region-order fixes (#313, #318, #317, #384, #438) add choice
lines to the traces of models that exercise them, E1 makes a terminate statement that
v0.8.0 ran as an empty action end its performance, E2 leaves the rest of an inline do body
unrun when the state is left, #479 reads a braced state block as one action and refuses the
older shape of its Turtle graph, and #359's -run-query runs after the run rather than on the
initial state. The decision is the release checklist's, recorded there.
- Release follow-through. R1 is done. R2–R5 as the accounts and hardware appear:
publisher tokens for npm, Maven Central and crates.io, a real Mac for the tap, an Apple Developer
and an OV/EV certificate to sign with, and a marketplace publisher for the extension. None gates
the others or anything below; R4's Windows installer is proven by two tagged releases. The
engineering item that sat beside them — the test-suite figures in
README.mdandspec-compliance.md, hand-typed and lagging the gate table — is closed by #291 ondevelop:tools/cmd/doc-countsgenerates and checks them as it does the pilot figures. - Track L. L3–L6 landed (#830, #821, #818, #825/#861) and L7 in #292 on
developafter the tag. Nothing remains in the track; its census moves with the runtime, adjudicated per change. - Track N. N2.1 (records and enums) first, since compiling an analysis case and the differential's record and constructor coverage both need it; decide N2.2 (the budget) before N2.4; N2.3 tracks L4 package by package; N2.6's actions and states are Track M's M1/M2.
- Track D. The RDF ratchet is 356/356 stable with no refusal left; the API element-form
ratchet is 354/356 with 2 graph-diff cases. D1 and D2 are done: expression trees use
standard ownership and operator vocabulary, and connector ends use
ReferenceSubsetting; D8's ontology-profile gate is therefore open. D7, D9.1, D9.2, D11 and D12 are done; D8 and D10 remain; D13 (the SysML v1 migration's units, its report on the wire and its experimental notice) in the order it lists. - Track F. Closed. F1 and F2 landed together (#116) as the token-per-succession model, F3
(#120) as the per-traversal merge on top of it;
known_failures.txthas no line left to delete. - Track S. Landed in the order agreed: S1 (#110), S2 (#123), S3 (#125), S4 (#134); #141 added the region-order choice point afterwards. Nothing remains in the track.
- Track E. Eligible — step 1 above. E1 (termination of an ongoing performance, which E2 and E4 build on) is landed; the order is E2; E4 is landed; E6 is landed; E3's record is landed and closes the item; E5's record is landed and closes the item, its optional follow-up waiting on a model that needs it; E7 is landed; E8 is landed: effective run-to-completion values and scopes are lowered and entry-step scheduling is exposed to execution, checking, exploration and replay; E1, E9 and E10 are landed; no work follows E3.
- Track X. X2, X3, X4, X5, X6, X7's values and X8's typing landed (#164, #115, #113, #211, #122, #121, #112). What is left, in order: X8's harness halves (normalization and adjudication in the pilot differential, a standalone RDF expression-tree round trip) so every later X item is measured; X7's native layout for sets and tensors last, when something needs them — step 5 above.
- Track A. A6, A2, A3 and A5 landed (#117, #133, #118, #136); A7 (#263) and A4 (#296) landed
on
developafter the tag, A7's carrier walk shared with Q2's query side (#267), and #344 gave A3's Monte Carlo the modeled randomness it had refused by name. Nothing remains in the track. - Track Q. Q4 is done; Q2's expression half landed with X5 and its query side in #267, #289
and #293 on
developafter the tag, which closes Q2; Q1 is written; Q3 landed in #359. Nothing remains in the track. - Track V. Everything queued has landed (#822, #900, #831, #817, the rule pull requests, #811 reconciled with #907, #909); work the census's 53 unknown rows, negative case first, each change moving its row.
- Track B. B1, B2, then B3 — step 6 above; nothing holds B1 or B2 back; B4's file and HTTP providers whenever asked, its Flexo provider unblocked now that D9.2 is landed; B5 with Q1.
- Track W. W1 (
dot), W2 (plantuml) and W3 have landed, W3's PDF rasterization and panel export in #356 ondevelopafter the tag. What is left — richer node shapes and compartments in both writers — is independent of every other track and runs beside any step above. - Track I, M. Entirely given by the cross-cutting order above.