Skip to content

v0.4.3: Identity Release

Choose a tag to compare

@HuiJun HuiJun released this 02 Sep 18:45
· 2504 commits to main since this release
99e0200

OpenSysML 0.4.3

Release 0.4.3 is where an element gets an identity the notation can carry. The SysML v2 textual
notation deliberately records no element identity, so a model saved as .sysml and re-parsed had
fresh ids everywhere and a rename was a delete plus a create. An element may now declare the
repository element it is — standard user-defined metadata (@ElementId, with a ProjectRef
binding a document to its repository once, at the root namespace), shipped as an IdentityMetadata
library extension that any conforming tool already parses and preserves. Identity is validated,
survives notation → RDF → notation, and sysml -sync-diff computes the change set between a
model and a repository graph keyed by that identity, so a rename or a retype is an update to the
same element.

Alongside it, a solver verdict becomes the evaluator's verdict, the four non-Go clients and the
public Go API were each exercised by worked examples that surfaced the fixes below, the
conformance suite and pilot differential render their runs as CI test results, and a profiling
pass across the toolchain removed the costs a performance census found rather than the costs
assumed — a multi-file -validate goes from 6.8 s to 0.30 s over a 100-file corpus.

No model that validated under 0.4.2 stops validating, and no import path moves. Upgrading is a
binary swap.

An element's identity, carried in the notation

  • An element declares its repository identity. @ElementId { id = "…"; } annotates the
    element it is written about, and @ProjectRef { projectId = "…"; } on a root namespace binds
    the document to its repository, so element-level ids inherit their scope. Identity is opt-in per
    element: an element without an annotation keeps today's derived, latest-wins identity. The two
    metadata definitions ship as a non-normative library extension (IdentityMetadata, entering the
    same gates as the vendored files — the bundled-library check now reports 97/97 clean), and a
    constraint-tier pass validates id shape, scope binding and uniqueness across the workspace,
    including anonymous about-form usages, annotations declared in libraries, and targets outside
    the built roots.

  • Identity survives the RDF round trip. The writer mints subject IRIs from the effective id —
    the declared one where an annotation exists, the encoded qualified name where none does — marks
    declared ids, writes ProjectRef bindings as provenance triples, and refuses colliding ids
    across a mixed-scope workspace rather than silently merging two elements. The reader keys
    subjects on the element id, reports a dangling id as its own error, and re-materializes the
    annotations on the way back to notation, so notation → RDF → notation preserves which
    repository element each declaration is.

  • A model diffs against a repository, keyed by identity. sysml model.sysml -sync-diff repo.ttl
    reports the change set — creates, updates, deletes, and renames seen as updates to the same
    element — and never writes: applying is a separate step. -sync-base names the graph at the
    last-seen commit, so repository changes since then surface as conflicts rather than silent
    overwrites; deletes are reported always and confirmed with -sync-confirm-deletes;
    -sync-mint-ids mints a UUID for each unannotated element being created and -sync-annotate
    writes the model back out with each minted id declared, preserving the source text. The
    last-seen commit is tool state beside the model (<model>.sync.json), never written into the
    notation.

  • The design is a project record (docs/project/element-identity-annotations.md), and the
    notation has been submitted to OMG for standardization (docs/project/omg-issues.md).

Sound solver verdicts

The SMT translation reasons over exact rationals while the evaluator computes in IEEE 754
binary64, and the difference is reachable: the exact encoding holds 0.1 + 0.2 == 0.3 sat, which
the evaluator rejects.

  • A sat is a witness the evaluator confirms; an unsat is claimed only where the arithmetics
    coincide.
    Every satisfying assignment is replayed through the evaluator's float64 arithmetic
    and reported unknown with the reason when the replay rejects it; a query whose conditions the
    evaluator rounds is marked, %check and %solve report its exact-real unsat as undecided,
    and %configure all and %optimize decline the completeness claim outright. Narrower, and
    sound — what is given up is completeness on rounded queries, and a census over the repository's
    solver-facing corpora found none of them recoverable.

  • A whole-number quotient is a Rational. 5 / 2 answers 2.5 for Natural and Integer
    operands alike, which is what the reference evaluator answers; the quotient is computed as an
    exact ratio rounded once to float64, so it agrees with the exact SMT encoding even beyond 2^53.

  • The remaining alternative, an exact-rational evaluator value representation, was adjudicated
    against the pinned pilot and the specification text and declined

    (docs/project/exact-rational-evaluation.md).

The clients, exercised

  • Worked examples for every client, run by the tests. Runnable examples drive the Node, Java,
    Rust and Go clients over one capable model — quantities, enumerations, multiplicity, nesting,
    unvalued features; parsing, diagnostics, symbol navigation, evaluation and instantiation — and
    each client gains a reference page of its own (docs/reference/java-api.md,
    docs/reference/node-api.md, docs/reference/rust-api.md). The examples were written to find
    defects and did; the fixes are below.

  • The Java client's package is org.openmbee.opensysml, the DNS-verified namespace every
    future Java artifact belongs under, rather than io.opensysml. The client has never been
    published, so no released consumer moves with it.

  • The Python client 0.4.0 was published to PyPI.

Reporting and examples

  • The conformance suite and the pilot differential render as CI test results. The conformance
    runner emits JUnit XML (stored even when the gate fails), and pilot-diff writes JUnit XML with
    one suite per corpus root alongside SARIF 2.1.0 with one result per disagreeing diagnostic
    group, located on the compared model file.

  • The implementation models itself. examples/self-model is the analysis pipeline, surfaces,
    invariants and views of this implementation written as a SysML v2 model across five files whose
    packages import each other, with a make target rendering its diagrams and documents and a test
    evaluating its invariants and checking its figures against the implementation they describe.

  • -render-document takes a model of several files. A document may query elements its sibling
    files declare: sysml model/*.sysml -render-document Reports::MassReport -o report.md loads the
    named files as one model.

  • A pilot corpus records the pin it was fetched at. Each corpus directory carries a stamp
    naming the repository and tag it came from: a stale stamp triggers a re-download, a current one
    is left alone, and a directory without a stamp is left alone with a warning.

  • The HTML document backend is recorded as an agreed design for rendering documents as
    semantic, styleable HTML straight from the document IR (docs/project/html-document-backend.md)
    — proposed, not implemented.

A faster toolchain, by measurement

A September performance census and an execution-performance record
(docs/project/performance-census-2026-09.md, docs/project/execution-performance-2026-09.md)
measured where the toolchain stands; the costs they flagged are removed, and none of these
changes any diagnostic, verdict or rendered output.

  • A multi-file -validate batch is indexed once. Validation loaded each file through a path
    that reopened the session document, reindexed it and re-expanded every wildcard import, so a
    batch of N files paid N full indexes over a growing buffer. The batch is now a single
    submission, with each file's own syntax errors, load notices and summary still printed in file
    order. Over the training corpus: 0.26 s → 0.12 s at 25 files, 0.59 s → 0.13 s at 50,
    6.8 s → 0.30 s at 100 — a fixed floor plus a term that scales with the input, no quadratic
    term.

  • A calc invocation reuses a pooled frame. Each invocation allocated a fresh parameter map,
    evaluation context, statement host and engine — ~1.7 KiB per call, and GC took half the CPU of
    recursion-heavy evaluations. Returned frames go on a free list; a frame is only ever held by
    one active invocation, so recursion never aliases.

  • A run target resolves from a per-document name table. Starting a calc, state machine or
    instantiation re-walked the whole document scope tree per run, so a run cost O(model). At
    4,000 elements a state-machine start goes 204 µs → 6.4 µs, a calc 222 µs → 4.5 µs, an
    instantiation 217 µs → 3.3 µs, and the figures no longer scale with model size.

  • The parser's token buffer is a bounded window. The parser buffered every non-trivia token
    of a file up front; consumed tokens are now dropped once no checkpoint can rewind to them.
    With the REPL parsing each submitted file once rather than twice, a load allocates 30% fewer
    objects and holds 16% less live heap.

  • The about-metadata index is cached at index freeze time, so a session no longer walks the
    bundled standard library's scope trees to build it: the empty-session floor returns to
    ~0.19 ms and 117 KiB allocated (−80% wall, −83% allocation).

  • A repeated REPL command does not re-parse its text. Argument lists and command name text
    are memoized by exact text; evaluation still runs on every call against the current session
    state.

Fixes

  • A wider-typed expression binds to a narrower feature. return : Integer = 7 / 2; was
    refused statically because the quotient's type is Rational, yet an expression's static type
    only bounds its values — 4 / 2 is whole. A binding, argument or index is now refused
    statically only when the two types are disjoint or the value is a literal, whose type is
    exact; everything else is deferred to evaluation, where the value it actually turns out to be
    is still checked. This matches the pinned pilot, which accepts the declaration.

  • Every exported Session method holds the session lock. The run, check/solve, view,
    document and diagnostics entry points did not take it, so a Tab completion racing one of them
    touched the lazily built index, name table and runtime context unsynchronized — 144 races
    under -race, now none.

  • Node client: restarting the service waits for the previous process to exit before
    reconnecting, and does not wait on one that already exited.

  • Arithmetic outside a type's range is reported, not returned. A wrapped Integer sum, the
    least Integer's negation and its remainder, an infinite Real from an out-of-range literal, a
    folded infinity, and a quantity magnitude outside the Real range each answered as if computed;
    every one is now a typed error naming the range. An ordinary negated literal evaluates rather
    than being mistaken for a fold, seeded outputs are reported for what they are, and an escaped
    attribute default is decoded before use.

  • An unbound requirement subject is reported as unbound. A requirement checked with nothing
    supplying its subject read as a modelling mistake in the condition; the diagnostic now names the
    subject and the three ways to supply one — bind it, check it on an object, or assert
    satisfaction by an element.

  • A debounced call a later trigger superseded does not run. A timer firing as the next trigger
    arrived ran the work its successor now owned and deleted the successor's entry; a callback now
    confirms it is still the timer its key waits on.

  • Node client: a short name is looked up on a model adopted by hash; a missing symbol's error
    names what was looked for; an RPC failure surfaces as a typed client error rather than a raw
    transport error; an impossible encoding or timeout is refused at construction; and a failed
    handshake carries the status it failed with.

  • Java client: a call that outlives its request timeout is reported as the timeout it is, not
    as the service being unavailable, and a value kind the client cannot read is a refusal rather
    than a value silently dropped from the sequence holding it.

  • Rust client: an empty $OPENSYSML_SERVICE no longer selects an unnamed binary, a response
    above the transport's 10 MB default is read, a qualified value is read outside its declaring
    scope, string escapes decode, Integer overflow and non-finite Real folding are reported, and an
    instance graph iterates in declaration order rather than map order.

  • The toolchain download paths close their quality-gate findings. The stall watchdog owns a
    thread rather than an executor, the pandoc fetch refuses a plaintext redirect, and the mermaid
    install runs no dependency's lifecycle script and finishes before calling itself present.

Verification

The pre-tag gate of docs/project/releasing.md on the tagged commit: gofmt -l .,
go build ./..., go vet ./..., make lint, go test -race -count=1 ./...,
TestStdlibConformance, the training and pilot corpus gates with
OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1, the cmd/pilot-diff and
cmd/pilot-xpect provenance baselines, the documentation link/label/figure checkers, and
doc-counts -check.