This page describes which triples a model becomes, and which constructs the mapping does not represent. For saving and converting as a task, see guide chapter 7.
RDF conversion (sysml -convert ttl, %save model.ttl, the service's Convert
to or from ttl, and each in reverse) is experimental as of 0.1.0, and so is
the API element form (api-json, .json), which is the
same graph written as JSON. Saving
and converting notation (.sysml, .kerml) is stable; this mapping is not. Each
of the following is a deliberate property of the mapping rather than a defect to
report:
- What is not mapped is refused, not partly converted, and the refusal names
the construct. Every one of the 346 models under
examples/(committed, training and pilot corpora) converts to Turtle, and a second conversion of the written-back notation reproduces the Turtle byte for byte for every one — the notation is written from the source text the graph carries. These figures are the per-file ratchet intests/corpus/roundtrip_test.go, described in rdf-corpus-roundtrip.md. See Behavior and Limitations. - The vocabulary may change without a compatibility path. A graph written by
one release may not read back into the next, and no migration is provided.
Treat a
.ttlas an interchange artifact you can regenerate, not as the copy of record. - Interoperability is not yet demonstrated, and the gap is measured rather
than argued. The
sysml:vocabulary and theelmt:element base match Flexo MMS'sNamespaces.kt. OpenSysML's ids (the part of an IRI after the final:, for elements and expression nodes alike) match that service'srequireValidId([a-zA-Z0-9_-]+). Every element carries thesysml:elementIdthat paged listing and query select use, and ownership is written as the memberships and owner references the roots endpoint filters on. A collection-valued property is written twice, as the typed triples and as the JSON annotation literal that service reads a collection from (Collections). A round trip through a running Flexo MMS stack delivers every element of the reference fixture and every one of its standard properties, the multi-valued ones included; what it loses is thesysx:properties, since the reader ignores predicates outsidesysml:andurn:sysmlv2:annotation:json:. The measurement lives ininternal/translate/interop/flexo, an opt-in gate described in.agents/skills/flexo-interop, and its committed report records what changes as the remaining work lands.
The same stack is also a place a model is read from and pushed to: -convert
names a project branch of the configured stack by URL
(host[:port][/base]/projects/{p}/branches/{b} of the FLEXO_SYSMLV2_URL
endpoint — one naming another endpoint is refused, so a run cannot read one
stack and write another — or flexo://{p}/{b}), reads the branch's head commit
through Layer 1's SPARQL
endpoint as the graph this document describes, and converts it as Turtle from
there — -convert sysml writes the notation back, -convert ttl the
normalized document. Pushed the other way, -convert ttl -o <branch-url>
replaces the branch's whole model graph through Layer 1's graph endpoint,
conditional on the branch's etag so a moved head is refused rather than
overwritten. The bearer token is FLEXO_INTEROP_TOKEN; the head commit a read
or push stood at is recorded in the sync state, so a later push refuses a
branch another writer moved.
Every surface reports this status where it is used: the command line writes a
note: to stderr, %save prints one, and ConvertResponse carries experimental
and experimental_notice, which the Python client raises as an
ExperimentalFeatureWarning. The wording is a single constant,
convert.ExperimentalNotice.
| Prefix | IRI | Holds |
|---|---|---|
sysml: |
https://www.omg.org/spec/SysML# |
Metaclasses and metamodel properties |
elmt: |
urn:sysmlv2:element: |
The elements of the converted model |
sysx: |
urn:opensysml:sysml: |
The few properties the metamodel does not define |
expr: |
urn:opensysml:expr: |
The expressions an element's positions hold, see Expressions |
json: |
urn:sysmlv2:annotation:json: |
One JSON literal per collection-valued property, see Collections |
rdf:, xsd: |
the standard RDF and XML Schema namespaces | rdf:type, literal datatypes |
The sysml: vocabulary and the elmt: element base match the ones the
Flexo MMS SysML v2 service
writes into its triplestore (Namespaces.kt). That service derives an
element's @id from the substring after the final :, and requireValidId
permits only [a-zA-Z0-9_-]+. OpenSysML's encoded element ids satisfy both, and
so do the expr: node ids. (A node's id used to contain a ., which that service
refused to read; the position is now joined with _p and encoded instead.)
The json: annotation base is that service's ANNOTATION_JSON (Namespaces.kt),
the one its reader takes a collection from (Collections). One
mismatch remains: the reader ignores predicates outside sysml: and
urn:sysmlv2:annotation:json:, so sysx: triples do not survive that path. See
Status.
OpenSysML's own additions live in a separate sysx: namespace so a consumer can
tell them apart from the standard vocabulary and ignore them if it wants only
standard SysML.
A graph an earlier release wrote with terms this one no longer reads is refused
rather than read without what those terms said: the pre-rename namespace
urn:systemica:sysml:, and the metadata properties sysx:prefixMetadata and
sysml:annotates that release 0.4.3 wrote for a # prefix and an about
target (now a sysml:MetadataUsage and sysml:annotatedElement, see
What each element carries), and the flags
sysml:isSnapshot and sysml:isTimeslice that release 0.5.1 wrote for a
portion (now sysml:portionKind). The error names the term; re-export the
model from its notation source.
An element's IRI is its qualified name, encoded as an id, appended to elmt::
package Demo { part def Vehicle; }
elmt:Demo a sysml:Package ;
sysml:qualifiedName "Demo" .
elmt:Demo__Vehicle a sysml:PartDefinition ;
sysml:qualifiedName "Demo::Vehicle" ;
sysml:elementId "Demo__Vehicle" ;
sysml:owner elmt:Demo .The encoding (rdf.EncodeElementID) works over the UTF-8 bytes of the
qualified name, with _ as the escape character:
- the
::separator becomes__ - a byte in
[A-Za-z0-9-]stands for itself - every other byte — a literal
_included — becomes_plus two lowercase hex digits:A_B::C→A_5fB__C,A::B_C→A__B_5fC,Importer::@0→Importer___400,Vehicle Mass→Vehicle_20Mass
The id therefore always matches [A-Za-z0-9_-]+, distinct qualified names
never collide, and rdf.DecodeElementID reverses the encoding exactly. The
IRI is deterministic: converting the same model twice yields the same
IRIs, and re-converting after an edit leaves the untouched elements at the same
addresses. The id is an address, not the copy of record for the name. The
name is carried by sysml:qualifiedName, which is where reading a graph back
takes it from.
The encoded qualified name is only the derived id, the one an element gets
when nothing declares one. An @IdentityMetadata::ElementId annotation
declares the id explicitly, and then the element's IRI and sysml:elementId
carry the declared id instead, so a rename keeps the subject:
package Demo {
@IdentityMetadata::ProjectRef { projectId = "proj-1"; }
part def Vehicle {
@IdentityMetadata::ElementId { id = "8f3a41d0"; }
}
}
elmt:8f3a41d0 a sysml:PartDefinition ;
sysml:qualifiedName "Demo::Vehicle" ;
sysml:elementId "8f3a41d0" ;
sysx:declaredId "true"^^xsd:boolean .The identity annotations are consumed into identity rather than exported as
metadata content, exactly as names are consumed into IRIs. sysx:declaredId
records that the id came from an annotation. That fact cannot be recovered from
the value itself, since a declared id may happen to equal the encoding of the
current qualified name, and dropping it would turn the next rename into a delete
plus a create. A membership's id derives from its member's effective id
(8f3a41d0_om), and an expression node's from its owner's, so both inherit the
id's stability.
A @IdentityMetadata::ProjectRef annotation on a scope root is written as
provenance triples on that root: sysx:projectId, sysx:branch, sysx:org.
A named element of the KerML or SysML standard library has a third kind of id,
sitting between declared and derived: the normative id the OMG
specifications fix for it, which the pilot implementation and every conforming
API server carry. Converting a bundled library file (or a model that owns a
copy of one) writes that id, so ScalarValues::Real is the same subject here as
it is in the pilot's sysml.library.xmi:
<urn:sysmlv2:element:40bb440c-5036-58e1-8675-5afccb8b8f1d> a sysml:LibraryPackage ;
sysml:qualifiedName "ScalarValues" ;
sysml:elementId "40bb440c-5036-58e1-8675-5afccb8b8f1d" ;
sysml:isStandard "true"^^xsd:boolean ;
sysml:ownedMembership elmt:ab72a695-5fe9-58a3-9d48-9e9a8711862d .
<urn:sysmlv2:element:14c0aa22-5489-59b5-b438-ded26e83ba31> a sysml:DataType ;
sysml:qualifiedName "ScalarValues::Real" ;
sysml:elementId "14c0aa22-5489-59b5-b438-ded26e83ba31" ;
sysml:owningMembership elmt:ab72a695-5fe9-58a3-9d48-9e9a8711862d .(A local name that starts with a digit is written as a full IRI rather than an
elmt: prefixed name, so a UUID reads either way depending on its first hex
digit.)
Derived ids are spelled this way by default because the qualified name they
encode lets a consumer address an element without resolving the graph. Passing
-id uuid to -convert ttl or -convert api-json mints name-based uuids
instead, as the library convention does: each root package gets
uuid5(URL namespace, prefix + name) with https://www.omg.org/spec/SysML/ as
the prefix, and every subject the encoder derives under it gets
uuid5(package id, the id it would carry by default) — so an owning
membership's is uuid5(package id, member id + "_om") and an expression
node's composes its owner's derived id the way the encoded positions do. Ids a
declaration fixes (an @ElementId annotation, a library element's normative
UUID) are never re-derived; sysml:elementId literals carry whichever form the
subject carries, and the api-json reader classifies a node's namespace from the
membership that owns it, so either form reads back to the same notation.
The id is a version-5 UUID (internal/semantic/identity): the library
package's is uuid5(URL namespace, prefix + name) with the prefix
https://www.omg.org/spec/KerML/ for the kernel libraries and
https://www.omg.org/spec/SysML/ for the systems and domain libraries; a named
member's is uuid5(package id, qualified name) and its owning membership's is
uuid5(package id, qualified name + "/owningMembership"), both with the names
quoted the way the pilot quotes them. Which bundled files are which library
follows the stdlib tiers (symbols.LibraryTier), so a file OpenSysML adds under
internal/workspace/libs/stdlib that is not part of either specification keeps
derived ids. So does an unnamed, aliased or shadowed library element: nothing
is guessed.
A normative id is not a declared one. sysx:declaredId is not written for it,
and reading the graph back leaves it implied — no @ElementId annotation —
because the notation that comes back is a version of the bundled library file
(see below) and derives the same id again on its own. Notation the encoder does
not read as the library — a graph whose roots are not a library document's, or
whose source text is stale for some element — cannot imply the id, so the
reader writes it as an @IdentityMetadata::ElementId annotation instead and
the id survives a further hop through notation as a declared one. The reader
recognises a normative id only on the subject whose sysml:qualifiedName is the
library element's (its effective name, or the exact positional name the writer
gives an effectively named member); a user element that happens to carry a
library UUID without sysx:declaredId keeps it as a declared id, as any other
foreign id. An explicit @IdentityMetadata::ElementId stating a different id
still wins over the normative id when a library element carries one; an
annotation restating the norm's own id declares nothing, so the id stays
normative and sysx:declaredId is not written.
What is a version of a library file. A document is a version of a bundled
library file when every one of its roots is a top-level package that file
declares, under the same qualified name, and all of one file. On the graph side
the root must carry the package's normative id; in notation, where an
unannotated package can state no id, the root must either state the normative id
in an @ElementId annotation or be declared as the library declares it
(standard library package Occurrences, library package …), and the document
must be in the file's language: the text of ScalarValues.kerml under a .sysml
name was parsed as SysML, so it is the user's file. Both sides apply
the one test, identity.Catalog.DocumentRootedAt in
internal/semantic/identity/library_version.go (libraryDocument for graph roots,
documentLibrary for parsed roots in internal/translate/export/library_names.go
feed it). It is not a test of names, file names or UUIDs alone:
package Actions { part def X; } is a user package with encoded ids (Actions,
Actions__X) and none of the library's members, a user element carrying a
catalogued UUID under another qualified name keeps that id as declared, and a
graph rooted at a nested library element (ScalarValues::Real) is not the
document, even under its normative id.
Such a version is analyzed in the bundled file's place, whatever its bytes: the
encoder takes the bundled document out of the library index and indexes the
version where it stood, so the version's own declarations are what its names
resolve to (portionOfLife subsets portionOf in a respaced copy of
Occurrences.kerml reaches the copy's portionOf, not the bundled package
beside it), its elements carry their normative element ids and normative
owning-membership ids, and no sysx:declaredId is written for them. The reader
does the same with a graph whose roots are a library file's: it writes the
notation back in that file's grammar (KerML for a .kerml library) when the
roots record none, and with the graph's own declarations standing in for the
bundled ones when a name is checked to reach the element the graph links. That
is what lets a chain reach a feature the library only implies —
aState.aTransition.accepter.acceptedMessage in Actions.sysml reads
accepter off the TransitionAction every transition inherits — and lets a
KerML library read back without its source text at all. The target still has to
be the graph's exact element; a spelling that reaches anything else is refused
as before. Together these make the source-free round trip of every bundled
library file exact from the first hop: the Turtle of the rebuilt notation equals
the Turtle it was rebuilt from, source text aside, and the rebuilt notation
states no @ElementId (library_graph_test.go:TestLibraryFilesComeBackFromTheGraphAlone).
The editor workspace applies the same recognition. A version of a library file
open in a workspace (or on its disk) stands in for the bundled file: the
workspace takes the bundled document out of its library index and indexes the
version where it stood, so names resolve to the version's declarations, its
elements carry their normative ids — hover states (normative, KerML) as on the
bundled file — and the language server offers no minting action on them
(internal/frontend/lsp/identity_test.go:TestWorkspaceCopyOfLibraryFileIsTheLibrary).
Editing a root so it no longer qualifies (renaming the package, dropping its
library keyword, stating a foreign id) makes the document the user's again and
puts the bundled file back; closing a version whose on-disk text is the user's
does the same (internal/workspace/model/library_version_test.go).
A document holding more than one project scope qualifies each element's IRI
with its scope's provenance (elmt:<encoded-org>.<encoded-project>:<id>), so an
id repeated across scopes stays two subjects; two scopes whose elements would
still land on one IRI are refused rather than silently merged.
Reading a graph back keys the subjects on sysml:elementId; the qualified
name is a mutable label. A graph without sysml:elementId (from before the
property existed, or from another tool) falls back to the encoding of its
qualified name, which is what its IRIs carry. The notation writer re-materializes an
@ElementId annotation wherever the graph marks sysx:declaredId true or
the id differs from the encoding of the qualified name, and one @ProjectRef
per root carrying provenance. Subjects are classified by their rdf:type,
never by parsing the id — a declared id may legitimately end in _om or embed
_p without being a membership or expression node. A subject stating several
classes is read as the one that is a subclass of all the others in the SysML
ontology (sysml:OwningMembership, sysml:ResultExpressionMembership is a
ResultExpressionMembership; sysml:ActionDefinition, sysml:Function, sysml:CalculationDefinition a CalculationDefinition, in whichever order the
triples come); a set of classes with no such member is refused, naming the subject.
rdf:type— the SysML metaclass (sysml:PartUsage,sysml:ActionDefinition, …). Every definition and usage keyword the parser accepts has a metaclass; the tables ininternal/translate/export/kinds.goare the source of truth, and the reverse direction is derived from them so the two cannot disagree. Every metaclass written is one the metamodel declares concrete (checked against the abstract classes of the pilot'sSysML.ecoreandkerml.ecore:ConnectorAsUsage,ControlNode,Element,Expose,Import,InstantiationExpression,LoopActionUsage,Relationship), since the SysML v2 API never returns an abstract one. So an import is asysml:NamespaceImport(import P::*,P::**) or asysml:MembershipImport(import P::M,P::M::**), a view'sexposelikewise asysml:NamespaceExposeor asysml:MembershipExpose, and a KerMLconnectoris asysml:Connector. Reading, the abstractsysml:Importearlier releases wrote is still accepted, told apart by itssysx:isNamespaceImportandsysx:isExposeflags or its imported property, as issysml:ConnectorAsUsage; all are written back as the concrete class on the next hop.sysml:declaredName,sysml:declaredShortName,sysml:qualifiedName— on a requirement'ssubject,assume constraintandrequire constraintmembers as on any usage, sosubject <s> x : T;comes back with its short namesysml:elementId— the id the element's own IRI ends in, which is what the SysML v2 API addresses it by. Every element carries one, including the memberships below and the expression nodes of Expressions- Ownership, described under Ownership:
sysml:owner, plussysml:owningMembershipandsysml:owningRelationship, orsysml:owningRelatedElementfor an element a relationship owns sysml:owningNamespace— the containing namespace (absent on a root and on an element a relationship owns, whose owner is no namespace), kept alongsidesysml:owneras the compact spelling earlier releases wrotesysml:visibility,sysml:direction- Feature flags, written only when true, so an absent flag reads as false:
isAbstract,isVariation,isVariant,isReference,isComposite,isDerived,isOrdered,isNonunique,isEnd,isConstant,isIndividual,isPortion,isConjugated,isAll,isAccept,isResult, andisEventfor aneventmodifier on a usage whose metaclass is not itselfsysml:EventOccurrenceUsage. A flag the two grammars spell differently is written back in the grammar of its root (sysx:sourceLanguage):isConstantas KerML'sconstor SysML'sconstant;isPortionas KerML'sportion, in place ofcomposite(a portion is composite, soisPortionwithoutisCompositeis refused), and on a SysML root as nothing of its own — there it is the factsnapshotortimeslicestates (OccurrenceUsage::portionKindimplies it), so it is written back by the portion kind and refused without one, SysML having noportionprefix andcompositedropping the fact. The other flags are spelled alike in both grammars.isIndividualis written for a definition as for a usage (OccurrenceDefinition::isIndividual, SysML v2 §8.3.9.11): anindividual part def,individual item def,individual occurrence def, … carries it and reads back with itsindividualmodifier, and so does anindividual def, whose kind keyword states the fact and is written back alone rather than doubled as a modifier. As withsysml:EnumerationDefinitionandisVariation, the metaclasssysml:IndividualDefinitionstates the fact on its own: a graph typed so but carrying no flag — the shape earlier releases wrote — reads back asindividual defand gains the flag on its next hop, after which it is stable sysml:portionKind,"snapshot"or"timeslice", for a usage declared as a portion (snapshot :>> start,timeslice occurrence t); the two are the metamodel'sOccurrenceUsage::portionKind, so no flag spells them. Such a usage also carriessysml:isPortion, the fact its kind implies; a graph stating the kind alone reads back by its kind, and gains the flag when re-exported- Declaration-head relationships, as element IRIs where the target resolves
to an element with an identity — by name resolution, so a name reached through
an import, an alias, an inherited member or a nested package qualification
links to the same element its fully qualified spelling does, and a standard
library element is linked by its normative id
(
attribute mass : MassValuelinks<urn:sysmlv2:element:9cd0e404-efee-50e5-a59b-681065bd188c>, whether or not the library is in the graph) — and as plain literals only where the name resolves to nothing the model declares:sysml:type(the:clause),specializes,subsets,redefines,references,crosses,disjointFrom,intersects,inverseOf,unions,chains,includes,via,subject,annotatedElementfor anaboutclause, and what an import names:importedNamespaceon aNamespaceImport, and on aMembershipImportimportedMembership, which links the imported element's owning membership (the metamodel's range), a library member's by its normative membership id; an import written through an alias imports the alias's owning membership. The same rule links a succession'ssourceFeature(an impliedfirst start then anames thestartthe owner inherits fromActions::Action), a feature chain'stargetFeature, a feature reference'sreferentand an invocation'sfunction. A literal carries the name itself, without the quotes an unrestricted name is written with; a target that is an expression rather than a name (a feature chain, say) is carried as the text it was written as, typedsysx:Expressionto tell the two apart. These properties are written in one canonical order whatever order the clauses were spelled in —type,specializes,subsets,redefines,references,crosses,disjointFrom,intersects,differences,inverseOf,unions,chains,includes,via,annotatedElement,subject,featuringType(internal/translate/export/kinds.gorelationshipOrder, the same order the clauses are written back in) — with the targets of one property in the order they were written; soattribute :>> num : Real;andattribute : Real redefines num;give byte-identical Turtle, and a.ttlkept under version control does not churn with the spelling of a head. Reading a graph back, a literal that is neither — a number, a boolean, a language-tagged string, an empty or broken qualified name — is refused rather than written into the notation as it stands. A feature chain'ssysml:targetFeaturelinks the member the chain reaches in its operand's type, a redefinition the general's feature; written back, each is spelled by its own name where that reaches one feature among the operand's or the owner's generals, else qualified. A transition orthenend in a state machine links the vertex it names anywhere in the machine, in a nested state or a sibling region; a loop'swhileoruntilcondition links the actions the loop body declares. A body expression's parameter, aforloop's variable and a trigger's parameter are no elements of the graph: a reference to one stays its name, even where it shadows a feature of the same name. Reading a graph back, a link and a legacy literal spell the same reference: a literal is the name as written; an IRI of this graph is spelled by the shortest name that resolves to that element from where it is written, and a library IRI by the library element's shortest visible name (Realunderimport ScalarValues::*, elseScalarValues::Real) — so a short name (#moe) comes back as the element's name (#MeasureOfEffectiveness) once the source text is gone. - A KerML relationship written keyword-first as a member of its own
(
specialization Gen subtype A specializes B;,subset f subsets g;,inverse f of g;,featuring of f by T;,disjoint A from B;) is an element typed by its metaclass, and its two ends are two properties whose order the metamodel fixes:sysml:Specializationwithspecificandgeneral;sysml:FeatureTypingwithtypedFeatureandtype;sysml:SubsettingwithsubsettingFeatureandsubsettedFeature;sysml:RedefinitionwithredefiningFeatureandredefinedFeature;sysml:ConjugationwithconjugatedTypeandoriginalType;sysml:FeatureInvertingwithinvertingFeatureandfeatureInverted;sysml:TypeFeaturingwithfeatureOfTypeandfeaturingType;sysml:DisjoiningwithtypeDisjoinedanddisjoiningType. Each end is a link or a literal by the rule above, so a feature chain (disjoint earlier.successors from later.predecessors;) is carried assysx:Expressiontext.sysx:declaredKeywordkeeps the keyword the member was written with (subtypeagainstsubclassifier) andsysx:declaredPrefixthespecialization,invertingordisjoiningthat introduces its name; the notation is written back from the two ends, so swapping them in the graph swaps them in the notation. This is distinct from the clause of a declaration (class C specializes A disjoint from B;), which stays a property ofC(sysml:specializes,sysml:disjointFrom). sysml:lowerBound,sysml:upperBound— multiplicity, as expression nodes (Expressions)sysml:value— a feature's value, as an expression node, withsysml:isDefaultandsysml:isInitialstating thedefaultand:=of the operator it was written with (sodefault = 1does not come back as the binding= 1, which a redefinition may not override)sysml:aliasedElement,sysml:client,sysml:supplier,sysml:body,sysml:language,sysml:locale,sysml:annotatedElement- A metadata annotation —
@Safety;,@Safety { level = 2; },metadata m : Safety about a, b;or the prefix#Safety part def P;— is asysml:MetadataUsageowned by the element it is written in or ahead of, through anOwningMembership(never aFeatureMembership: the annotation is not a feature of what it annotates), even when that element is itself a relationship such as adependencyor asubjectmembership. It carriessysml:typefor its metadata definition, onesysml:annotatedElementperabouttarget,sysx:hasBody, andsysx:declaredKeyword"@"or"#"for the sigil it was written with (metadatais the absence of both). The body's members are its owned members like any other body's: a value binding (level = 2;) is asysml:ReferenceUsagecarryingsysml:value, a redefinition (:>> level = 3;) carriessysml:redefines, a nested feature keeps its own kind, andsysx:memberIndexorders them. A#prefix is an owned member of the declaration it prefixes, indexed after the body's members so their indices are the same with or without it, and is written back at the head of that declaration rather than in its body, in the grammar's position: ahead of the kind keyword and ofassert/perform(#Safety assert not constraint c;), aftersubject,actor,stakeholder,objective,variant,assume,requireandvar(assume #Safety constraint c;). - The cross feature an end declares ahead of its kind keyword —
end [0..*] item x : A;,end x1 [1] typed by Sub1 item y : B;— is asysml:Feature(in SysML, asysml:ReferenceUsage) owned by the end through anOwningMembership, indexed after the end's body members and prefix annotations, carrying its name, itssysml:lowerBound/sysml:upperBoundand its specializations. Its bounds are never the end's:end [0..*] item x : A[1];states[0..*]on the cross feature and[1]on the end, and the decoder writes each back where it was declared. The head has no place for the cross feature's own body, so an id the cross feature must declare is written back as anaboutannotation in the end's body (end x1 [1] item y : B { metadata : IdentityMetadata::ElementId about x1 { id = "…"; } }), the one place the grammar offers, naming the cross feature by its name or, failing that, its short name; a graph that gives the cross feature a body of its own or members, or an id but no name to say, is reported as unsupported, naming it.
The sysx: properties:
| Property | Why it exists |
|---|---|
sysx:memberIndex |
Declaration order. The notation is sensitive to the order of members; an RDF graph is an unordered set, so the index is what lets a conversion back to notation reproduce the original sequence. |
sysx:hasBody |
Distinguishes part def A; from part def A { }, which are different source and would otherwise convert back identically. Also marks an expression body node, so {} rebuilds from structure. |
sysx:sourceText, sysx:sourceTail |
The element's lines as written, comments and blank lines included, which a conversion back to notation prefers while they still state what the graph states. An element with members carries the lines ahead of them as its text and those after them as its tail. See Source text. |
sysx:sourceLanguage |
On each root element, the grammar the file was written in — sysml or kerml — so the text is read back under the grammar it was written under, and a flag the two grammars spell differently (const against constant, see the feature flags above) is written in that grammar. Absent for a buffer with no model extension (standard input, a REPL session), which the parser reads as SysML with KerML's all prefix. See Source text. |
sysx:declaredKeyword |
The kind keyword as written, when it is one of the synonyms several keywords share (datatype and attribute, function and calc, KerML's feature and attribute, snapshot and occurrence), on a named declaration and on an anonymous one alike (feature :>> x;, snapshot :>> start { … }). The AST records one kind for all of them, so without this the notation would come back rewritten. Where the graph types the fact the keyword states — sysml:portionKind for snapshot/timeslice, the metaclass sysml:EventOccurrenceUsage for event, sysml:AssertConstraintUsage for assert — the typed fact is authoritative and this predicate only chooses between two spellings of it (snapshot :>> start against snapshot occurrence :>> start; event m.start against event occurrence references m.start; assert c against assert constraint references c); a keyword the typing contradicts (snapshot with sysml:portionKind "timeslice" or none, event on a sysml:PartUsage, assert on a sysml:ConstraintUsage) is refused rather than one of the two written. KerML's feature has no typed counterpart — an attribute usage is what the AST records for it and sysx:sourceLanguage does not decide between the two — so it is carried as this spelling alone. Also the keyword a constraint body's condition is stated with (assert, assume, or absent for a bare condition, which asserts implicitly), the constraint of an assume/require member that declares a constraint usage (so its references C is read as a specialization, where require C alone states the constraint the member refers to), and the sigil a metadata annotation was written with: @ for a member (@Safety;), # for a prefix ahead of a declaration (#Safety part def P;), absent for the metadata keyword. |
sysx:declaredPrefix |
The keyword qualifying the kind keyword after it — the assume of assume constraint c : C. It says what the declaration is for, and the AST kind alone does not carry it. The assert of assert constraint c : C is not written here: that usage is a sysml:AssertConstraintUsage, and the metaclass states it; a graph stating both with another prefix is refused. |
sysx:endForm |
The notation an end-binding head writes its ends in — to, nary, equals, firstThen, fromTo, flowTo, satisfy, then — so the head is rebuilt from the graph rather than read back from its text. See End-binding heads. |
sysx:endVerb |
The verb a head writes ahead of its ends when its own keyword is the noun form (connection c connect a to b, connector c from a to b). Without it the verb would be missing or doubled. |
sysx:endReferencesKeyword |
On a named end, the ReferencesKeyword written between the name and the feature when it is the word references (bind e1 references a = …). Absent, the end is written with ::>; a value other than the two spellings is refused. |
sysx:sourceMember, sysx:targetMember |
The member a succession sequences from or to where the notation names no end (then b;, or a then beside an unnamed member), or where the name the notation supplies for an end links no element (a then after action redefines walk; whose walk is inherited). The end is the element itself rather than only a name, so a same-named member elsewhere cannot be mistaken for it. |
sysx:condition |
The condition a condition member states, as its notation. |
sysx:resultExpression |
The expression an expression body ({ in y : Real; y + x }) ends in, after its parameters. The bare expression a calculation or case body computes is not an extension: it is the Expression its sysml:ResultExpressionMembership owns. See Result expressions. |
sysx:bodyParameter, sysx:bodyMember |
The in parameters an expression body declares, each a node carrying its name, type, bounds and value, and the other declarations it makes ahead of its result, each a node typed by its own metaclass and carrying what a member declaration carries. Both share one sysx:memberIndex sequence, the order they were written in. |
sysx:declaredId |
The element's id came from an explicit @IdentityMetadata::ElementId annotation, see Element identity. |
sysx:projectId, sysx:branch, sysx:org |
The @IdentityMetadata::ProjectRef provenance of a scope root, see Element identity. |
sysx:isKindImplicit |
The declaration wrote no kind keyword (in x : Real;), which takes its kind from its owner. Without it the canonical keyword would come back written out, declaring what the author did not. A kind named in a comment in the head (in /* attribute */ x : Real;) is trivia, not a keyword the declaration wrote. |
| the behavioral properties | sysx:guard, sysx:expression, sysx:payload, … — the parts of a behavioral node the vocabulary has no predicate for, listed under Behavior. |
For expressions and end-binding heads, the encoder still emits these sysx:
terms. They carry notation or ordering facts for which the 202407 metamodel has
no property; they are annotations, not replacements for the standard shape:
| Terms | Why the annotation remains |
|---|---|
sourceText, sourceTail |
Preserve source spelling, trivia and closing text when present; both are optional annotations and the decoder does not need them for the structural expression or end shapes. |
argumentName |
Preserve a named argument's spelling; the metamodel has parameter ownership but no notation-level name on an argument occurrence. |
typeArgument, isConstructor |
Preserve constructor syntax and a type argument on expression nodes where the metamodel has no corresponding notation flags. |
hasBody |
Distinguish an explicitly empty expression body from one with no body; the metamodel does not preserve that source distinction. |
resultExpression |
Identify the expression ending a body; the result expression's membership is structural, but the metamodel has no notation-level result marker for rebuilding the body form. |
bodyParameter, bodyMember |
Preserve expression-body parameter/member roles and their source placement, which are not represented by a single standard predicate in this mapping. |
memberIndex |
Preserve source order among expression-body parameters and members; RDF collections are unordered and the standard ownership graph does not carry this notation order. |
endForm |
Reconstruct the selected end-binding head form (to, nary, equals, firstThen, fromTo, flowTo, satisfy or then), which the standard end relationships do not identify. |
endVerb |
Preserve the noun-form head's explicit verb (connect or from), which the connector metaclass does not distinguish. |
endReferencesKeyword |
Preserve whether a named end used the references keyword rather than ::>, a notation choice not represented by the end's standard reference relationship. |
sourceMember, targetMember |
Preserve the members named by positional then succession ends when no standard end name or resolvable element carries that notation. |
payload |
Preserve a flow payload as an expression; the 202407 ontology table has no direct payloadFeature property for this graph shape. The metamodel models the payload as a feature/ItemFlowEnd, while this mapping keeps the payload expression rather than materializing that feature structure. |
Every construct the metamodel has an element for is typed by that element,
so a consumer reading sysml: terms alone sees the abstract syntax the pilot
implementation serializes; the shapes below were each checked against the
pilot's XMI of the standard library (scripts/download-pilot-library-xmi.sh):
alias a for b;is asysml:Membershipcarryingsysml:memberName(andsysml:memberShortNameforalias <s> a for b;) whosememberElementisb(KerML 1.0 § 8.3.2.4.3Membership::memberName);sysx:declaredKeyword "alias"tells it from afirst x;membership.multiplicity m [1..*];is asysml:MultiplicityRangeowned through anOwningMembershiplike any member, its bounds owned as for an inline bound (KerML 1.0 § 8.3.4.11.2MultiplicityRange),sysx:declaredKeyword "multiplicity".first x;is asysml:Membershipof the member the body starts at, anddone;asysml:Membershipof the library'sActions::Action::done(SysML.xtextInitialNodeMember;first x then yis theSuccessionAsUsageit sequences).sysx:declaredKeywordkeeps the keyword.- An
ifbranch is asysml:ActionUsagetheIfActionUsageowns through aParameterMembership, after theParameterMembershipthat owns its condition (SysML v2 1.0 § 8.3.17.10IfActionUsage::thenAction/elseAction), withsysx:branchKindfor the keyword. filter <expr>;is asysml:ElementFilterMembership; anassume/requiremember asysml:RequirementConstraintMembership; a constraint body's condition asysml:ConstraintUsage.- A state's
entry/do/exitis asysml:StateSubactionMembershipand a transition'sdoasysml:TransitionFeatureMembershipofkind "effect"(Behavior).
What remains typed in the sysx: namespace is notation the SysML v2 grammar
does not have, so no metaclass in the metamodel describes it; a consumer that
reads only sysml: terms (the sysml-toolkit among them) does not interpret
these, and the nonstandard-notation check reports the notation they come
from:
sysx:Pseudostate— a state body'schoice,junction,fork,joinandhistoryvertices, withsysx:pseudostateKind. The SysML v2 grammar has no pseudostate production, and the library'sStatespackage defines no state to specialize for one, so aStateUsagewould misstate them.sysx:DeferMember— thedefer sig;member of a state, carryingsysx:deferredEventper event; SysML v2 has no deferred triggers.sysx:ActionExecutionNode—action a { x + 1 }, an action node performing an inline expression, which no SysML v2 production spells.
Reading, the metaclasses earlier releases wrote for the standard constructs
above — sysx:Alias, sysx:FilterMember, sysx:MultiplicityDeclaration,
sysx:ConstraintMember, sysx:AssumeMember, sysx:RequireMember,
sysx:InitialNode, sysx:FinalNode, sysx:IfBranch — are still accepted and
written back as the standard element on the next hop.
The qualified usages spell their qualifier from the metaclass alone, so a graph
that carries no sysx:declaredKeyword/sysx:declaredPrefix still writes the
keyword the grammar qualified it with (SysML.xtext PerformActionUsage,
ExhibitStateUsage, IncludeUseCaseUsage, AssertConstraintUsage,
SatisfyRequirementUsage):
| Metaclass | Named | Unnamed reference form |
|---|---|---|
sysml:PerformActionUsage |
perform action pa : A |
perform a1;, perform sub.sa :>> a2; |
sysml:ExhibitStateUsage |
exhibit state es : S |
exhibit s1;, exhibit sub.ss :>> s2; |
sysml:IncludeUseCaseUsage |
include use case iu : U |
include u1; |
sysml:AssertConstraintUsage |
assert constraint ac : C |
assert c1; |
sysml:SatisfyRequirementUsage |
satisfy requirement sr : R |
satisfy r1; |
An unnamed one reads its target from sysml:references (or includes/subsets
where another writer collapses it there) — a chain target comes back as the
a.b text its chain feature states. An unnamed assert that owns members or a
not stays the anonymous declaration assert constraint references c1, which
the reference form cannot say. A sysx:declaredKeyword/sysx:declaredPrefix
that contradicts the metaclass (perform on a plain sysml:ActionUsage) is
refused rather than one of the two written.
The rest of the membership-side metaclasses the notation implies are standard: the mapping materializes each as the relationship element the OMG metamodel defines for it, so a consumer reading elements rather than notation sees the same relationships SysML.xtext produces.
| Notation | Elements minted |
|---|---|
part wheels : Wheel[4]; |
the member, a sysml:FeatureTyping (<S>_ft0) whose general/type is Wheel and whose specific/typedFeature is the member, a sysml:MultiplicityRange (<S>_mult) carrying the [4] bound, and the member's sysml:OwningMembership (<S>_om) |
part v : Vehicle; |
the member, its FeatureTyping (<S>_ft0), its OwningMembership |
:>> mass, redefines mass |
a sysml:Redefinition (<S>_rd<i>) with redefiningFeature/redefinedFeature |
:> base, subsets base |
a sysml:Subsetting/sysml:ReferenceSubsetting (<S>_ss<i>/<S>_rs<i>) |
specializes Base, :>> Base on a definition |
a sysml:Subclassification (<S>_sc<i>) |
part p : ~P; |
a sysml:ConjugatedPortDefinition (<S>_conjugated) typed by P, and a sysml:PortConjugation (<S>_pc) joining it to P, so port p's FeatureTyping reads type: ~P |
subject v : Vehicle; in a requirement |
a sysml:SubjectMembership owning v, with sysml:subjectParameter on the membership's owner |
require constraint { v.mass < 1500 [kg] } |
a sysml:RequirementConstraintMembership owning the sysml:ConstraintUsage, with sysml:kind "requirement"/"assumption" by keyword |
{ in y : Real; y + x } |
a sysml:ResultExpressionMembership owning the result expression |
filter <expr>;, import P::*[@T] |
a sysml:ElementFilterMembership carrying sysml:condition |
an expression's referent/targetFeature link |
a sysml:Membership minted beside it (<S>_referent, _preferent, _targetFeature) restating the referent edge as memberElement/owner, the shape interchange readers navigate (KerML 1.0 § 8.3.4.8.5 FeatureReferenceExpression::referent is derived from that membership). A body expression the reference owns (cars->select { in c : V; c == c }) is its member through the FeatureMembership that owns it, as in the pilot's XMI, so no second membership is minted for it |
f(1), T(q = 1), a + b, s.twice(2), ts.q, ts[1], ts->select { … } |
the InvocationExpression/ConstructorExpression/OperatorExpression/FeatureChainExpression/IndexExpression/CollectExpression/SelectExpression; per operand a sysml:ParameterMembership (<S>_pin<i>_om) owning a sysml:Feature with direction "in" (<S>_pin<i>) that owns a sysml:FeatureValue (<S>_pa<i>_om) whose value is the operand tree; a sysml:ReturnParameterMembership (<S>_pout_om) owning the direction "out" result Feature (<S>_pout); and for an invocation a sysml:Membership (<S>_pfunction) whose memberElement is the invoked function or constructed type — the pilot serializes InstantiationExpression::instantiatedType through that membership, not a FeatureTyping (KerML 1.0 § 8.3.4.8.8 InvocationExpression, § 8.3.4.6.4 ParameterMembership, § 8.3.4.7.8 ReturnParameterMembership, § 8.3.4.10.2 FeatureValue). sysml:function, sysml:argument, sysml:parameter, sysml:input and sysml:result are written beside as the derived properties they are (Expressions) |
view v : VD { expose a; expose P::*; render r; filter @T; view sub : VD; }, viewpoint vp : VPD { frame concern c; } |
sysml:ViewUsage/ViewDefinition/ViewpointUsage/ViewpointDefinition/RenderingUsage/RenderingDefinition, a sysml:MembershipExpose or sysml:NamespaceExpose per expose, a sysml:ViewRenderingMembership owning the RenderingUsage a render names (through a ReferenceSubsetting of it), a sysml:ElementFilterMembership per filter, a sysml:FramedConcernMembership owning the ConcernUsage a frame names, and a nested view as a ViewUsage member (SysML v2 1.0 § 8.3.26 Views and Viewpoints) |
| the document itself, in the API element form | an unnamed sysml:Namespace (<root>_ns) owning each top-level element through an OwningMembership (<root>_om); see The API element form |
a dependency, keyword-first specialization/subclassification/redefinition/subsetting/typing/disjoining/inverting/featuring/conjugation, or succession declared in a body |
the declared relationship, owned through an OwningMembership (<S>_om) like any other member — Import and Membership-family members excepted, which own directly |
Comments, documentation and textual representations convert as their own
elements (sysml:Comment, sysml:Documentation, sysml:TextualRepresentation)
carrying sysml:body.
Every element carries the notation it was written as, so a conversion back to
notation can return the file rather than a canonical rendering of it. The text
is the element's lines, trivia included: the // and /* */ comments and
blank lines ahead of a member belong to it, and a comment on its last line too.
An element with members carries the lines ahead of its first member as
sysx:sourceText and those after its last as sysx:sourceTail, since the
members carry their own; a package P { … } is written as its head, its
members in sysx:memberIndex order, and its }. A member written on its
owner's own lines — an accept's payload (accept sig : Cmd;), the branches of
an if — carries no text of its own: it is part of its owner's text, and an
edit to it rebuilds the owner whole rather than splicing one line. A succession
written as the then ahead of its target is likewise part of the target's
text: a succession added to or removed from the graph rebuilds that target.
Expression nodes carry sysx:sourceText too, as described under
Expressions.
The text is the notation as the author wrote it: the encoder slices the
file's own bytes, never a formatted copy, and the decoder writes them back
untouched, so any file converts to RDF and back byte for byte — tabs,
irregular indentation, blank lines inside a head, CRLF line endings, a string
literal or doc body spanning lines, all included. Roots written on one line
(package A; /* note */ package B;) each carry their slice of it, from their
first token up to the next root's, and what follows the last root (notes, blank
lines, a missing final newline) is that root's tail, since the document itself
has no subject. Tokens are never rewritten by either step, so a synonym (:>
for specializes, datatype for attribute def), an unusual member order, or
a reference written relative to another scope all come back as written. Layout
is never what the graph states: where the mapping records how a head was
written (sysx:endForm, sysx:declaredKeyword, a then) it compares the
head's tokens, so a head laid out over several lines, with a comment inside it
or a note after its ; is recorded like one written on a line; and where the
graph carries a node's text as a structural value (a relationship target, a
trigger), the notes and comments its span runs on over are left out.
The graph is authoritative. The text is a rendering of the structural
triples, not a second copy of the model, and the decoder checks it before
trusting it: the candidate notation is converted back to RDF and compared with
the graph being read, source text aside. The candidate is read under the
grammar the roots record as sysx:sourceLanguage, since KerML text can read
clean as SysML and mean something else (binding [1] a = b names the binding
a there); a root recording no language was read as a buffer with no
extension, and its text is read as one again, all as a prefix rather than a
name. Roots recording different languages are not read at all, and the graph
is written canonically. sysx:memberIndex is set aside too:
the notation lists members in index order whatever the numbers, so a member
removed from the middle of a body leaves those after it standing as written,
their indices no longer running on from zero. Each triple the two disagree on is
charged to the nearest element whose text it falls under — or to the outermost
expression node written from its text — which is then written in canonical
notation instead, and the notation is built and checked again until the two
graphs agree. So a graph edited after it was written (a flag set, a value
changed, a member removed, an id or ProjectRef dropped) comes back stating
the edit, with the stale text replaced only where it was stale:
// The rover, as modelled.
package Rover {
/* Definitions come first. */
part def Wheel :> Part; // a synonym the printer would spell out
abstract part def Hub;
part def Vehicle {
doc /* what a vehicle is for */
part wheels : Wheel[4]; // four of them
}
}
Here sysml:isAbstract was added to Hub after the export: its line — and the
note that was written above it — is rebuilt from the graph, and every other
line is kept, the blank line after it included, since that belongs to
Vehicle. Rebuilt lines end the way most of the elements' text does, CRLF or
LF; an expression's text lies inside its element's and is not counted again.
Text that no longer parses, or whose disagreement cannot be placed on one
element, demotes every element to canonical notation rather than writing an
invalid or contradictory file; one that lands on notation already rebuilt is
the graph's own (a declaredName edited without its qualifiedName) and
demotes nothing further. Identity annotations follow the same rule:
text that still carries its @IdentityMetadata::ElementId or ProjectRef is
kept as written, and text that has lost one is rebuilt with the annotation the
graph states, exactly as a graph without text is written
(Element identity).
A graph without source text converts to canonical notation, unchanged from
before: a graph from another tool, or one with sysx:sourceText stripped, is
written from its structural triples alone, with trivia gone and every keyword
spelled canonically. That path is what the round-trip tests exercise — see
Limitations — and this one adds to it rather than replacing it.
Tests: verbatim_test.go (byte-for-byte return, the stripped graph's canonical
notation, an edited flag, an edited expression, an edited string, an edited
accept payload, a removed member, an added and a removed then, dropped
identity annotations, text that does not parse) and export_test.go
(TestGoldenConversions locks both notations for every fixture).
The notation states containment by nesting; the abstract syntax states it as a membership element between the owner and the member, and that is what the SysML v2 API's payloads carry. The mapping materializes those memberships.
A namespace owning an ordinary member mints one membership element:
package Demo { part def Vehicle { attribute mass; } }
elmt:Demo
a sysml:Package ;
sysml:elementId "Demo" ;
sysml:ownedMember elmt:Demo__Vehicle ;
sysml:ownedMembership elmt:Demo__Vehicle_om ;
sysml:ownedRelationship elmt:Demo__Vehicle_om .
elmt:Demo__Vehicle_om
a sysml:OwningMembership ;
sysml:elementId "Demo__Vehicle_om" ;
sysml:owner elmt:Demo ;
sysml:memberElement elmt:Demo__Vehicle ;
sysml:ownedMemberElement elmt:Demo__Vehicle ;
sysml:ownedRelatedElement elmt:Demo__Vehicle ;
sysml:owningRelatedElement elmt:Demo ;
sysml:membershipOwningNamespace elmt:Demo .
elmt:Demo__Vehicle
a sysml:PartDefinition ;
sysml:elementId "Demo__Vehicle" ;
sysml:owner elmt:Demo ;
sysml:owningRelationship elmt:Demo__Vehicle_om ;
sysml:owningMembership elmt:Demo__Vehicle_om .- A membership's id is the member's id with
_omappended, which no element id can be: an_in an element id starts either__for::or a hex escape. It is minted byrdf.OwningMembershipID, so it is deterministic and reverses to the member's qualified name. A member with a normative id has a normative membership id too, and that is written instead. - A type owning a feature — a usage or a state inside a definition — mints a
sysml:FeatureMembershipinstead, and addssysml:ownedMemberFeatureandsysml:owningTypeon it andsysml:ownedFeatureandsysml:ownedFeatureMembershipon the owner.FeatureMembershipspecializesOwningMembership, so the_omid and the properties above still apply. - A KerML
member feature—class C { member feature x; }, the grammar'sTypeFeatureMember— is a feature the type owns through a plainsysml:OwningMembership, not aFeatureMembership: it is a member of the type but not one of its features, so none ofownedFeature,ownedFeatureMembership,ownedMemberFeatureorowningTypeis stated. Reading a graph back, aFeatureaTypeowns through a plainOwningMembershipis written with thememberprefix, after its visibility (private member feature x;), unless the membership is one KerML writes another way: aVariantMembership, aResultExpressionMembership, a metadata annotation, an enumerated value, or the cross feature an end declares in its head (described with the metadata annotations above). SysML has nomemberkeyword, so a SysML-language type that owns a feature through a plainOwningMembershipis anUnsupportedErrornaming the feature: writing it asattribute x;would make it a feature of the type, a different model. - A relationship a namespace declares — an import, a dependency, a state's
entry membership — is owned directly, with
sysml:owningRelatedElementon it andsysml:ownedRelationshipon the owner, and no membership between. An import also statessysml:importOwningNamespaceandsysml:ownedImport. - An element a relationship owns — the action of an entry membership — states
the relationship in
sysml:ownerandsysml:owningMembership, and the relationship states it insysml:memberElement. - Visibility belongs to the membership.
private part wheel;writessysml:visibility "private"on the membership, which is where the metamodel declares the property; a relationship that states its own visibility, such as an import, keeps it on itself. - A membership is not a declaration of its own: it carries no
sysml:qualifiedName, and reading a graph back traverses it as the ownership edge it stands for rather than writing it out. A membership the notation does name, such as asysml:StateSubactionMembership, has a qualified name and is written back. - The compact shape still reads.
sysml:owningNamespaceis still written, and a graph carrying only it — what earlier releases wrote — converts back unchanged. A membership that states neither of its ends is reported as unsupported namingsysml:memberElement, rather than dropping the member; so is one whose spellings of an end (sysml:memberElement,sysml:ownedMemberElement,sysml:ownedMemberFeature,sysml:ownedResultExpression,sysml:ownedRelatedElement) name different elements, one whose end is a literal rather than an element, and a second membership owning an element another already owns — rather than keeping one edge and dropping the rest. The element's side is held to the same rule: itssysml:owningMembership/sysml:owningRelationshipmust agree with each other and with the membership that claims it, and itssysml:owner,sysml:owningNamespaceandsysml:owningRelatedElementwith the namespace that membership puts it under.
Tests: ownership_graph_test.go (element ids, roots, membership wiring, the
tree coming back from the memberships with sysx:sourceText and
sysml:owningNamespace stripped, the compact shape, malformed memberships).
A property with several values is stated twice: as one typed sysml: triple per
value, which is what RDF states, and as one literal on the same key in the
json: namespace holding the whole collection as a JSON array:
package Demo { part def A; part def B; part def C specializes A, B; }
elmt:Demo
sysml:ownedMember elmt:Demo__A, elmt:Demo__B, elmt:Demo__C ;
json:ownedMember "[{\"@id\":\"Demo__A\"},{\"@id\":\"Demo__B\"},{\"@id\":\"Demo__C\"}]" .
elmt:Demo__C
sysml:specializes elmt:Demo__A, elmt:Demo__B ;
json:specializes "[{\"@id\":\"Demo__A\"},{\"@id\":\"Demo__B\"}]" .The second spelling exists because of how the
Flexo MMS SysML v2 service
reads a graph. ElementApi.extractModelElementToJson indexes a subject's
outgoing triples by predicate; a sysml: predicate with more than one object is
an array to it, and it skips the typed triples and reads the property from
the literal at urn:sysmlv2:annotation:json:<key> instead, which must be
exactly one RDF literal, parsed as JSON. Its own commit path (CommitApi.kt)
stores a posted array both ways: one JSON annotation literal holding the array,
plus a typed triple per member — an IRI for a {"@id": …} member, a typed
literal for a primitive. The mapping writes what that path writes, so a graph
OpenSysML produces reads back through that service with its collections intact;
the live measurement is in internal/translate/interop/flexo/testdata/interop_expected.txt.
The JSON shape is the commit path's:
- a reference is
{"@id": "<id>"}, the id being the part of the IRI after the final:, for elements and expression nodes alike. In a multi-scope document a reference into another project scope keeps that scope's qualifier,{"@id": "<encoded-org>.<encoded-project>:<id>"}(an empty qualifier,":<id>", naming the unscoped root), exactly as its typed triple does; so an id that both scopes carry still names one element, and the two spellings compare exactly. A single-scope graph, which is what the service holds, never spells a qualifier; - a primitive is a JSON string, boolean or number:
xsd:booleanastrue/false,xsd:integer,xsd:decimal,xsd:doubleandxsd:floatas numbers, every other literal as a string of its lexical form; - the array is compact, without HTML escaping, and its order is the triple order of the graph, which the mapping writes deterministically (declaration order for members, source order for a head's targets), so the same model yields the same literal.
Which properties carry it. Every sysml: property a subject states more
than once, and only those; a single-valued property is unchanged. Which
properties that is follows from the mapping rather than from a list: the
ownership collections ownedMember, ownedMembership, ownedRelationship,
ownedFeature, ownedFeatureMembership, ownedImport, and on a relationship
element that itself owns members (an objective, a requirement's satisfy) the
ownedRelatedElement, memberElement, ownedMemberElement and
ownedMemberFeature it states per member; a head's
relationships when it names several targets — type, specializes, subsets,
redefines, references, disjointFrom, intersects, unions, differences,
chains; a dependency's supplier; and an expression node's argument
(Expressions). A relationship the head states by a name the
model does not resolve is a plain literal in the typed triples and a JSON string
in the annotation, so one collection can mix references and strings. Over the
corpora under examples/ these are the keys that occur; a model that states
another property twice gets the annotation on that property too.
Reading a graph back accepts either spelling or both. A collection stated by
the annotation alone — what that service writes back for a graph it holds —
is materialized as typed triples in the annotation's order before decoding, a
{"@id": …} member resolving to the subject with that id in the scope the id
spells — the referring subject's own when it spells none — or, absent one,
standing as an element IRI that dangles as any other unresolved reference does;
a subject outside the element and expression namespaces is never the target,
whatever its local name.
An annotation that names a cross-scope target by its bare id disagrees with the
qualified typed triple and is refused as a conflict rather than retargeted to
the referrer's scope. An id that both an element and an expression node carry
(the two namespaces are disjoint, so an element may declare the id a node
derives) resolves in the referrer's own namespace — an element's members are
elements, an expression node's arguments are nodes — and a referrer in neither
namespace has such an id refused rather than one subject picked.
A string member reads as a plain literal, since the annotation carries no
datatype: a head target written as an expression comes back from the annotation
alone as a name, where the typed triple would have carried sysx:Expression.
A collection stated by typed triples alone — a graph an earlier release or
another tool wrote — reads as before. Where both are present they must agree as
multisets, typed triples carrying no order, and the annotation's order is the
one the decoder takes; two spellings that disagree are refused with an
rdf.CollectionConflictError naming the subject and the key, rather than one of
them being picked. An annotation that is not one literal, or not a JSON array
of references and primitives, is refused naming the subject and the key; so is
an array that repeats a member, since a graph holds each triple once and could
not give the repetition back.
The sync (-sync-diff, and the branch read and graph push under -convert)
compares and writes the typed triples and treats the annotation as
their restatement, reconciling it first; the service's commit path regenerates
it from the array the sync posts. Minting ids into a model rewrites the typed
triples and restates each annotation from them, so the two cannot drift: a
declared id that merely resembles a minted element's derived ids stays as it is.
Code: rdf.AnnotateCollections (encoder pass), rdf.ReconcileCollections
(decoder pass), rdf.CollectionJSON/rdf.ParseCollectionJSON (the shape).
Tests: internal/translate/rdf/annotation_test.go,
tests/export/rdf_collections_test.go,
tests/reposync/diff_test.go.
An expression-valued position — a feature value, a multiplicity bound, a guard,
a filter, a condition, a send payload, a loop's collection — states the
expression as a tree of typed nodes in the expr: namespace and gives the
tree the standard ownership shape. The expression root is held by an
OwningMembership whose memberElement is the root and, for a feature value,
whose metaclass is FeatureValue and whose featureWithValue names the
containing feature:
package P {
attribute a : Integer;
attribute total : Integer = a * 2;
}
elmt:P__total
a sysml:AttributeUsage ;
sysml:value expr:P__total_pvalue ;
sysml:ownedMembership expr:P__total_pvalue_om ;
sysml:ownedRelationship expr:P__total_pvalue_om .
expr:P__total_pvalue_om
a sysml:FeatureValue ;
sysml:featureWithValue elmt:P__total ;
sysml:value expr:P__total_pvalue .
expr:P__total_pvalue
a sysml:OperatorExpression ;
sysml:elementId "P__total_pvalue" ;
sysml:operator "*" ;
sysml:parameter expr:P__total_pvalue_pin0, expr:P__total_pvalue_pin1 ;
sysml:input expr:P__total_pvalue_pin0, expr:P__total_pvalue_pin1 ;
sysml:ownedFeatureMembership
expr:P__total_pvalue_pin0_om, expr:P__total_pvalue_pin1_om .
expr:P__total_pvalue_pin0
a sysml:Feature ;
sysml:direction "in" ;
sysml:ownedMembership expr:P__total_pvalue_pa0_om .
expr:P__total_pvalue_pa0_om
a sysml:FeatureValue ;
sysml:featureWithValue expr:P__total_pvalue_pin0 ;
sysml:value expr:P__total_pvalue_pa0 .
expr:P__total_pvalue_pa0
a sysml:FeatureReferenceExpression ;
sysml:referent elmt:P__a .The rules the tree follows:
- A node's IRI is built from its owner and its position:
expr:<owner id>_p<slot>, and a nested operand appends its own index (_pa0,_pa1). Two expressions of one element therefore never collide, and the IRIs are deterministic, like element IRIs. The_pmarker and the encoding of the position keep a node's id inside[A-Za-z0-9_-]+, the alphabet the SysML v2 API'srequireValidIdaccepts, and it can never be read as an element id or a membership id, because an element id never ends in a lone_. - A value root is owned structurally. The containing feature keeps its
direct
sysml:valuefor compatibility and also owns anOwningMembership. A feature-valued root is reached through a typedFeatureValue, whosesysml:valuenames the root and whosefeatureWithValuenames the feature. The decoder treats the direct route as canonical when both routes name the same root, and refuses conflicting roots. - Operands are owned structurally. Each operand has an input
sysml:Featurewith directionin, aParameterMembershipin the expression'sownedFeatureMembership, and aFeatureValuemembership whosevalueis the operand expression.sysml:parameter/sysml:inputidentify the parameters, while the JSON annotation preserves their RDF order. - Metaclasses are the standard ones where the metamodel names them:
LiteralBoolean,LiteralInteger,LiteralRational,LiteralString,LiteralInfinity,NullExpression,FeatureReferenceExpression,FeatureChainExpression,OperatorExpression,InvocationExpression,CollectExpression,SelectExpression,ConstructorExpression,MetadataAccessExpression,Expressionfor a body.sysml:operatornames an operator expression. Legacysysml:argumentandsysx:argumentIndexare still accepted on import; new graphs use the standard parameter shape. - A literal's
sysml:valueis a typed literal whose lexical form is the token the notation spells it with:"2"^^xsd:integer,"1.5"^^xsd:decimal,"1.5E3"^^xsd:double(an exponent is outsidexsd:decimal's lexical space),"true"^^xsd:boolean, and a string with its escapes resolved. Read back, a value is spelled as that token again: a rational with no fractional digits ("3"^^xsd:decimal) gains them (3.0), a boolean istrueorfalse, a string is quoted and escaped; a value no token spells — a signed number,INF,NaN— is reported as unsupported, naming the node, since the notation states a sign as an operator applied to a literal. - A
LiteralStringcarries its value, the escapes of the notation read: a"say \"hi\""in the file issysml:value "say \"hi\""in Turtle, and a value edited in the graph is written back as the literal that reads to it. Control characters are written with Turtle's own escapes (\b,\f,\uXXXX), so every triple stays a single line of valid Turtle. - A feature reference links to the element it names (
sysml:referent) when that element is in the graph, and carries its name as a literal when it resolves outside it, the same rule the declaration-head relationships follow. An invocation links the function it names the same way (sysml:function) and owns asysml:Membership(<S>_pfunction) whosememberElementis that function — the pilot's serialization ofinstantiatedType— beside the feature chain it is applied to (sysml:operand, fors.reading->twice()ors.signal.condition()) and its arguments, named or positional, each owned as aParameterMembershipparameter with aFeatureValue; a constructor statessysx:isConstructorand the same membership names the type. The reader takes the derivedsysml:function/sysml:argumentand the owned structure as one statement: a graph carrying either alone reads, and one whose function membership names a different element thansysml:function, or whose parameters andsysml:argumentdisagree, is refused rather than read one way. A body the invocation's operand owns (->select { … }) is reached through itsFeatureMembership, with no second membership. Written back, the function is spelled by the reference rule under Limitations, so an invocation whose function the graph neither links nor names, or that no spelling reaches from where it is written, is reported rather than misspelled. - A node carries
sysml:elementId, the id its own IRI ends in, so it can be read and queried by that id like an element. It is still not a model element: it has nosysml:qualifiedName, and reading a graph back never turns one into a declaration. - A graph from another tool is read from its structure when it carries no
sysx:sourceText: the supported shapes above are written back as notation, and a shape this mapping cannot write (a missing operator, an operand count an operator does not take, a literal with no value) is reported as unsupported, naming the node, never guessed. - Parentheses follow the parser's precedence table. The tree records no
parentheses, so the writer places them where the grammar needs them: an operand
that binds more loosely than the operator around it is parenthesized
(
size(ae) == (if isEmpty(af) ? 0 else 2) and …,(p ?? q) implies r,(a + b)[1],(x as T).f,- (1 + 2) ** 2,not (p and q)), one that binds as tightly or tighter is not (a + b * c,if p ? x else - x,p hastype T or q). An index encloses a sequence, so a multi-dimensional index is written bare (cube#(2, 1, 2),m[1, 2]), nevercube#((2, 1, 2)). A conditional, being the loosest form, is parenthesized wherever it is an operand or the condition of another conditional; as the operand of**the left side must bind tighter than exponentiation, so(a ** b) ** ckeeps its parentheses whilea ** b ** cgroups to the right, as the parser reads it. Text kept fromsysx:sourceTextis placed the same way, so a foreign operand written into a kept expression is parenthesized when needed. - An expression body is structure too.
{ in y : Real; y + x }is asysml:Expressionnode whosesysx:bodyParameters are nodes of their own — each typedsysml:ReferenceUsagewithsysml:direction "in", its name,refflag,sysml:type, bounds,sysml:valueand any body of its own — and whosesysx:resultExpressionis the tree of the expression after them, so a nested body ({ in y : Real; f(x = { in z : Real; z + y }) }) and anin exprparameter's body (in expr keep : Boolean { in v : Real; v > x }) rebuild from the graph alone. The node statessysx:hasBody, so an empty body ({}) is told apart from an expression with no structure at all and comes back as{}. Documentation opening a body ({ doc /* … */ in y : Real; y }) is asysml:Documentationnode with itssysml:body. Any other declaration a body makes ahead of its result ({ in y : Real; private attribute k : Real = 2; y * k }) is asysx:bodyMembernode of its own, typed by its metaclass and carrying what the same declaration carries as a namespace member — name, keyword, flags, visibility, typing and the other relationships, bounds, value, and a body of its own, whose declarations nest the same way. It is local to the expression: it has nosysml:qualifiedName, no owning namespace and no membership, and is reached only from the body that holds it. A declaration the body cannot hold — one with a#Mor@Mannotation of its own, an end with a cross feature, or one whose graph declares an id — is reported, naming it, as is a parameter with nosysml:declaredName. Parameters and declarations share onesysx:memberIndexsequence, so a parameter written after a declaration comes back after it. - Older graphs still read. A position holding a plain literal
(
sysml:value "1200.0"), which is what releases before this wrote, is read as that notation, and asysx:bodyParameterholding a bare name literal is read as that parameter.
Tests: w6g4_rdf_expr_test.go (structure, ordering, per-position identity,
legacy literals, foreign trees, unsupported shapes, round-trip exactness),
result_expression_test.go (expression bodies, their parameters and members).
The mapping states a model, not an evaluation of it, and that holds for every
value kind: an Array, a vector, a vector quantity and a scalar quantity with a
unit are each written as the expression valuing the feature, never as the value
the runtime computes, and the conversion never reaches the runtime (the layering
test forbids internal/translate/export importing it). So a value the runtime
holds as a set (the elements of a Collections::Set, UniqueCollection or
Map — any unique, unordered collection) or as a tensor of any rank
(Quantities::TensorQuantityValue over a TensorMeasurementReference with
three, four or more dimensions) has no literal form in RDF, and needs
none. What the graph carries is the expression the feature is written with — the
(3, 1, 2, 2, 3) valuing elements as an OperatorExpression with
operator "," over LiteralInteger operands, the
TensorCalculations::'['(…, cubeRef) building the tensor as an
InvocationExpression whose second argument is a FeatureReferenceExpression,
the cube#(2, 1, 2) indexing it — as the typed tree above, under the feature's
sysml:type (the IRI of Set, TensorMeasurementReference, …). Evaluating the
model read back gives the same set — equal to one whose members were written in another
order, as the runtime already treats them — and the same tensor, shape and
components. No xsd datatype or sysx: vocabulary encodes an evaluated
collection or a tensor's shape: the sysx: predicates on these trees are exactly
those every other expression uses (the source-text triples). A graph that
wanted to state a set's members or a tensor's components states the expression
that yields them. The gRPC service is where evaluated
values travel (the set and tensorQuantity arms of the wire contract).
Tests: set_tensor_rdf_test.go — exactness with and without the source text
for Set, UniqueCollection and Map features and rank-3 and rank-4 tensors;
the graph's shapes are the standard expression classes and no set- or
tensor-specific term; removing a load-bearing structural predicate
(sysml:operator, sysml:function, sysml:referent) breaks the round trip,
and so does removing both operand routes (the ParameterMembership operands
and the legacy sysml:argument), while either route alone still carries it; and
the model read back evaluates to order-insensitive set equality and the same tensor shape and components as the original.
A calculation, case, analysis or verification body may end in a bare expression,
the result it computes (calc def Double { in x : Real; x * 2 }). The abstract
syntax owns that expression through a ResultExpressionMembership whose
ownedResultExpression redefines ownedMemberFeature — the Expression is the
member — and so does the graph: the expression is an element of its own, typed
by its expression metaclass, placed by sysx:memberIndex like every other
member so a body whose result follows other declarations comes back in the same
order (a graph that states no index, as a standard one does, gets it last, where
the grammar has it), and owned through a membership typed sysml:ResultExpressionMembership
that states it as both sysml:memberElement and sysml:ownedResultExpression:
elmt:P__Double___401
a sysml:OperatorExpression ;
sysml:qualifiedName "P::Double::@1" ;
sysx:memberIndex "1"^^xsd:integer ;
sysml:owningMembership elmt:P__Double___401_om ;
sysml:operator "*" ;
sysml:argument expr:P__Double___401_pa0, expr:P__Double___401_pa1 ;
sysx:sourceText " x * 2\n" .
elmt:P__Double___401_om
a sysml:ResultExpressionMembership ;
sysml:memberElement elmt:P__Double___401 ;
sysml:ownedMemberFeature elmt:P__Double___401 ;
sysml:ownedResultExpression elmt:P__Double___401 .The expression has no name, so it is addressed by position, as the shorthand
relationships under Limitations are. Being an element, its
sysx:sourceText is its lines as written, as under Source text,
rather than the bare notation an expression node carries. It is the same tree a
feature value is, so it converts back from the graph with no sysx:sourceText
at all, whether it is an operator, a literal, an invocation, a feature chain, a
conditional or an expression body. Any Expression a ResultExpressionMembership
owns is written back as its body's result, so a graph another tool wrote with no
sysx: term on it reads too. A result whose graph states no expression
structure is reported, naming the expression, rather than written as an empty
line.
Tests: result_expression_test.go (the membership, the place among other
members, the round trip with sysx:sourceText stripped, the trip from the
membership alone, the refusals) and the result_expressions and
expression_body_members fixtures under testdata/convert/.
An action or state body converts: each node in it has a metaclass and the
properties its notation is rebuilt from, so notation → RDF → notation returns
the body byte for byte (behavior_test.go). Where the OMG vocabulary names
the node, that name is used; the rest are sysx: terms, marked below.
| written | metaclass | carries |
|---|---|---|
first x; in an action body |
sysml:Membership with sysx:declaredKeyword "first" |
sysml:memberElement and sysml:sourceFeature (the member the flow starts at — a reference, not a name it declares), sysx:hasBody and the members of its body. Read, a sysx:InitialNode from an older graph is the same member |
first x then y { … } in an action body (the succession x → y, which marks no start) |
sysml:SuccessionAsUsage with sysx:declaredKeyword "first" |
sysml:sourceFeature (x, a reference), sysml:targetFeature (y), sysx:guard, sysx:hasBody and the members of its body |
done; |
sysml:Membership with sysx:declaredKeyword "done" |
sysml:memberElement, the library's Actions::Action::done; then done; is a SuccessionAsUsage targeting the same. Read, a sysx:FinalNode from an older graph is the same member |
action a;, action a { x + 1 } |
sysx:ActionExecutionNode |
sysml:references or sysx:expression |
perform a; |
sysml:PerformActionUsage |
sysx:expression (the action performed) |
assign x := 1; |
sysml:AssignmentActionUsage |
sysx:target, sysml:value, sysx:assignmentOperator when it is not := |
send M(x) to p;, … via p; |
sysml:SendActionUsage |
sysx:payload, sysx:receiver, sysx:isVia |
terminate;, terminate x; |
sysml:TerminateActionUsage |
sysx:expression |
action stop terminate; (a declared terminate action usage) |
sysml:TerminateActionUsage |
the usage's own properties, sysx:hasBody among them — which is what tells a declaration from the statement above, since a statement never states it |
accept sig : Signal;, accept when c; |
the usage's own metaclass | sysml:isAccept, and sysx:declaredKeyword "accept" where the optional action was not written |
fork, join, merge, decide |
sysml:ForkNode, JoinNode, MergeNode, DecisionNode |
sysml:declaredName |
succession first a then b;, if g then b;, else b;, and a state body's keyword-less first a then b; (a succession between two vertices, no initial node) |
sysml:SuccessionAsUsage |
sysml:sourceFeature, sysml:targetFeature, sysx:guard, sysx:isElse, sysx:declaredKeyword; the keyword-less spelling comes back as succession first a then b; from the graph alone, the same succession |
public succession S first a if g then b; (a guarded succession, which is a transition) |
sysml:TransitionUsage |
as a transition, with sysx:declaredKeyword "succession" for the keyword written; sysx:transitionSyntax is derived from where the AST places the source, not from the words ahead of it, so a visibility or a name does not change it. Written back, a named form always writes first (succession S first a …, transition T first a …), since only a nameless transition may state a bare source |
while c { … }, loop { … } until c; |
sysml:WhileLoopActionUsage |
sysx:whileCondition, sysx:untilCondition |
for x in c { … } |
sysml:ForLoopActionUsage |
sysx:loopVariable, sysx:collection |
if c { … } else { … } |
sysml:IfActionUsage, owning through sysml:ParameterMemberships its condition and then a sysml:ActionUsage per branch |
sysx:condition, and sysx:branchKind and sysx:hasBody on each branch; a sysx:IfBranch from an older graph reads as the same branch |
state s { … }, state s parallel { … }, entry; then s; state s; |
sysml:StateUsage |
sysml:declaredName, sysx:declaredKeyword, sysml:isParallel, its members |
entry/do/exit, entry do { … } (whatever separates the do from the body) |
sysml:StateSubactionMembership |
sysml:kind (entry, do, exit) beside sysx:subactionKind, sysx:declaredKeyword, the one action it performs, which a perform a; states as a sysml:PerformActionUsage and an empty entry; as an anonymous sysml:ActionUsage with no name and no body; a braced block entry { … } is an anonymous sysml:ActionUsage with sysx:isKindImplicit (no action keyword was written) whose sysx:hasBody is the braces. A graph from an older mapping that wrote a braced block as its statements under the membership, sysx:hasBody on the membership itself, is refused as unsupported: it holds no anonymous action to read the block back as |
defer sig, other; |
sysx:DeferMember |
sysx:deferredEvent per event |
choice, junction, fork, join, shallow/deep history |
sysx:Pseudostate |
sysx:pseudostateKind, sysx:declaredKeyword |
transition [n] [first] s [accept t] [if g] [do e] then t;, … then t { … } |
sysml:TransitionUsage |
sysml:sourceFeature, sysml:targetFeature, sysx:trigger, sysx:triggerKeyword, sysx:guard, sysx:transitionSyntax, its effect and body as members: the effect is owned through a sysml:TransitionFeatureMembership with sysml:kind "effect" and sysml:transitionFeature (SysML v2 1.0 § 8.3.18.8), the transition stating it as sysml:effectAction, and the collapsed sysx:effectMember, sysx:bodyMember links are written beside; a graph carrying either form alone reads, and one whose TransitionFeatureMembership and sysx:effectMember name different members is refused, with sysx:hasEffect on every transition written with do (its braced effect do { … } is an anonymous action as for a state's entry { … }, so an empty do { } survives as that action's sysx:hasBody) and sysx:hasBody for a trailing body; a graph with members linked by neither owns an effect alone, sysx:hasBody its braces. A graph from an older mapping that wrote a braced effect as its statements (sysx:bracedEffect, or sysx:hasBody on an unlinked effect) is refused as unsupported: it holds no anonymous action to read the block back as |
A state's members are held in the AST in one bucket per kind (entry, do, exit,
defer, substates); they are written back in the order they were
declared, taken from their source spans, so do before entry stays that way.
The conditions and expressions these nodes carry are expression trees, like every other expression-valued position (Expressions): they convert back exactly and SPARQL can see inside them.
What is still refused, naming the node:
- A succession that does not name both of its ends.
then fork;andthen monitorPedal;written after a preceding member express an order whose source end the notation leaves implicit, and the parser records the node the statement introduces separately from the edge into it. Reconstructing that shape would mean inferring which node an edge belongs to from member position, which could silently reattach edges, so it is reported instead. Nine of the eighteen remaining refusals underexamples/are this shape.
These are the constructs the mapping does not fully represent. Each is a documented limitation, not a silent one: converting an affected element from a graph that lacks the source text reports an error naming the element rather than guessing.
An expression tree is not the metamodel's own expression model. Feature
values, multiplicity bounds, filter and constraint conditions and guards are
expression trees (Expressions), which makes them queryable, but
the nodes are not Features owned through FeatureMemberships the way the
abstract syntax models an expression. A conversion back to notation is written
from the text each node was written as where the graph carries it, and from the
tree where it does not. A consumer that wants the metamodel's own shape does not
get it from this mapping; the one membership it does materialize is the
ResultExpressionMembership of a result expression.
Lexical comments survive the RDF hop only as source text. // and /* */
trivia is attached to no element in the graph's structure; it comes back because
the lines carrying it are the sysx:sourceText of the member they precede
(Source text). A graph without that text — from another tool,
or stripped — drops it:
// this line comes back with the source text, and is gone without it
package Demo {
doc /* this is kept either way: doc is a declaration, not trivia */
comment about Wheel /* kept for the same reason */
part def Wheel;
}
The comment and doc keywords declare elements, so they convert both ways. An
element whose text is stale — its graph was edited after export — is rebuilt
canonically, and a comment on its lines goes with the text. Save straight to
.sysml when the comments must survive an edit; that path writes the source and
keeps everything.
A library copy is recognised by its roots, not its contents. Notation is
read as a version of a bundled library file when its roots pass the test under
Normative library identity; what those packages
contain is not compared with the bundled file. A copy whose root package drops
the library's standard library (or library) modifiers and states no
@ElementId is a user package with encoded ids, however alike its members, and
a copy that keeps the roots but has renamed or removed members carries the
normative ids of the members it still declares and derives ids for the rest as
a library file would. Nothing is compared below the roots, so nothing is refused
there either: converting such a copy is exact for what it declares.
A reference is written in the spelling that resolves, where it is written, to
the element the graph names. Every reference an element carries — a
specialization, subsetting, redefinition, reference-subsetting or typing target,
the root and members of a feature chain, an import, a succession, connection or
transition end, the requirement a satisfy names — is a link to that element,
not a name. Writing it back, the converter spells the link as the short name when
the resolver reads that name, from the writing scope, as the linked element, and
otherwise as the shortest qualified name it does read that way. So a redefining
attribute that bears its target's name inside a definition whose supertype also
redefines it writes redefines Packets::'packet data field', since the short
name there would reach the inherited redefinition; a part payload :> payload
whose target is the package's payload writes subsets Shadowing::payload,
since payload inside the definition would be the subsetting part itself; and a
: Packet inside a definition that declares its own Packet writes
: Shadowing::Packet when the outer one is meant. The scope a spelling is read
in is the one the parser reads it in: a featured by or crosses target in a
feature's head is read in that feature's own scope first, where its type's
members are visible, so a member feature nested in an anonymous
portion :>> startShot that is featured by that portion writes featured by CC1::startShot, since the short name in the feature's head would reach the
inherited Occurrence::startShot instead. A name shadowed at every
level falls back to the global form ($::Shadowing::Packet), and an element
that no spelling reaches from where it is written is reported rather than
written as a different element. What a spelling reaches can depend on how the
references beside it are spelled — an import's short name may read through a
sibling import only while that sibling is written qualified — so the chosen
spellings are checked again in the notation that actually writes them, and
lengthened until every one reads as the graph states. The fixture
testdata/convert/shadowed_references.sysml covers the three shadowings, and
TestRoundTripIsLossless writes every fixture back from the graph with its
sysx:sourceText removed and requires the graph the notation produces to be the
one it came from (export_test.go:TestWrittenReferencesResolveWhereWritten,
TestPacketsRoundTripsStructurally).
A head comes back in one spelling. The graph carries what a head declares, not how it was spelled, so the notation written back is normalised where the notation offers a choice and the model does not:
- A relationship written as a symbol or as its keyword (
:>orsubsets,:>>orredefines,::>orreferences) is the same relationship element, so no spelling is recorded and the writer uses one form. This differs fromsysx:declaredKeyword, which is kept where the notation's synonyms name different declarations (datatypeandattribute). - The clauses of a head come back in the canonical order of What each element
carries — typing first, then
specializes,subsets,redefines,references, and so on — however they were written (snapshot s :> context : Ctxcomes back assnapshot s : Ctx subsets context). The order of the clauses states nothing about the model, and the graph does not record it: the properties are the same set either way, and the writer emits them in the canonical order, so a spelling could only be restored from the source text, which is whatsysx:sourceTextis for. - The modifiers of a usage are written in the grammar's order (
end #derive r1 : R;,end ref cause : S[*];), and a multiplicity goes with the typing clause it qualifies, or with the name when there is none (composite frontWheel[2] redefines w). The parser reads the same flags in either order and either position (export_test.go:TestFixturesComeBackFromTheGraphAlone). - A
docorcommentbody is carried with the line endings it was written with, but the notation written back uses the document's own — a body written with CRLF comes back with LF. The text is otherwise verbatim.
A second conversion of the notation written back gives the same graph. The
optional sysx:sourceText annotation preserves source spelling and trivia, but
the decoder does not need it when the structural expression shape is present.
Graphs written by earlier releases still import. This includes the legacy
sysml:argument plus sysx:argumentIndex operand shape and expression roots
reached only through their positional properties. When both standard
ParameterMembership operands and legacy arguments are present, they are
compared by ordered identity and argument name; disagreement is refused.
Earlier releases also left the receiver of x->f(a) out of the parameters,
naming it by sysml:operand alone, spelled new as sysx:isConstructor on a
sysml:InvocationExpression, and owned a return parameter through a plain
FeatureMembership with sysml:isResult: each still reads. A receiver the
parameters do place, anywhere but first, and an isResult that contradicts a
ParameterMembership/ReturnParameterMembership are refused.
Likewise, when direct and typed FeatureValue value routes are both present,
equal roots are rendered once and conflicting roots are refused.
A head that binds ends records the form it writes them in. A connect,
bind, flow, succession, transition, accept or satisfy declaration is
carried as standard end structure, with the small sysx: annotations needed
for notation that the metamodel does not carry. sysx:sourceText is optional:
the decoder does not need it when the standard shape and the applicable
annotations are present.
elmt:P__Car___402
a sysml:ConnectionUsage ;
sysx:endForm "to" ;
sysml:connectorEnd expr:P__Car___402_pend0,
expr:P__Car___402_pend1 ;
sysml:ownedRelationship expr:P__Car___402_pend0_om,
expr:P__Car___402_pend1_om ;
sysml:ownedMembership expr:P__Car___402_pend0_om,
expr:P__Car___402_pend1_om ;
sysml:ownedFeatureMembership expr:P__Car___402_pend0_om,
expr:P__Car___402_pend1_om ;
sysml:ownedFeature expr:P__Car___402_pend0,
expr:P__Car___402_pend1 ;
sysml:relatedFeature elmt:P__Car__left, elmt:P__Car__right ;
sysml:sourceFeature elmt:P__Car__left ;
sysml:targetFeature elmt:P__Car__right .
expr:P__Car___402_pend0
a sysml:ReferenceUsage ;
sysml:elementId "P__Car___402_pend0" ;
sysml:isEnd true ;
sysml:ownedReferenceSubsetting expr:P__Car___402_pend0_prs ;
sysml:owner elmt:P__Car___402 ;
sysml:owningMembership expr:P__Car___402_pend0_om .
expr:P__Car___402_pend0_prs
a sysml:ReferenceSubsetting ;
sysml:referencingFeature expr:P__Car___402_pend0 ;
sysml:referencedFeature elmt:P__Car__left ;
sysml:relatedElement expr:P__Car___402_pend0, elmt:P__Car__left .
expr:P__Car___402_pend0_om
a sysml:EndFeatureMembership ;
sysml:memberElement expr:P__Car___402_pend0 ;
sysml:owningRelatedElement elmt:P__Car___402 .sysml:connectorEnd is ordered by its json:connectorEnd annotation. Each end
is a ReferenceUsage with sysml:isEnd — a PortUsage for an interface's
end, whose grammar declares InterfaceEnd returns SysML::PortUsage
(SysML.xtext) against ConnectorEnd returns ReferenceUsage — an
EndFeatureMembership, and an
owned ReferenceSubsetting whose sysml:referencedFeature names the linked
feature (or a literal when the name is not resolved). Named ends carry sysml:declaredName and sysml:name; multiplicity
bounds are on the end. A qualified target such as rover.telemetry is an
owned sysml:Feature — a chain feature — that owns one sysml:FeatureChaining
relationship per link, in order, and keeps the derived sysml:chainingFeature
list beside it; the end's ReferenceSubsetting owns the chain as its
sysml:ownedRelatedElement (KerML OwnedReferenceSubsetting:
ownedRelatedElement += OwnedFeatureChain) and names it as its
referencedFeature:
expr:P__Car___402_pend0
a sysml:ReferenceUsage ;
sysml:isEnd true ;
sysml:declaredName "bead" ;
sysml:name "bead" ;
sysml:ownedReferenceSubsetting expr:P__Car___402_pend0_prs .
expr:P__Car___402_pend0_pchain
a sysml:Feature ;
sysml:chainingFeature elmt:P__rover, elmt:P__telemetry ;
sysml:owningRelationship expr:P__Car___402_pend0_prs ;
sysml:owner expr:P__Car___402_pend0 ;
sysml:ownedRelationship expr:P__Car___402_pend0_pchain_pfc0,
expr:P__Car___402_pend0_pchain_pfc1 .
expr:P__Car___402_pend0_pchain_pfc0
a sysml:FeatureChaining ;
sysml:chainingFeature elmt:P__rover ;
sysml:featureChained expr:P__Car___402_pend0_pchain ;
sysml:source expr:P__Car___402_pend0_pchain ;
sysml:target elmt:P__rover ;
sysml:relatedElement expr:P__Car___402_pend0_pchain, elmt:P__rover ;
sysml:owningRelatedElement expr:P__Car___402_pend0_pchain .
expr:P__Car___402_pend0_pchain_pfc1
a sysml:FeatureChaining ;
sysml:chainingFeature elmt:P__telemetry ;
sysml:featureChained expr:P__Car___402_pend0_pchain ;
sysml:source expr:P__Car___402_pend0_pchain ;
sysml:target elmt:P__telemetry ;
sysml:relatedElement expr:P__Car___402_pend0_pchain, elmt:P__telemetry ;
sysml:owningRelatedElement expr:P__Car___402_pend0_pchain .
expr:P__Car___402_pend0_prs
a sysml:ReferenceSubsetting ;
sysml:referencedFeature expr:P__Car___402_pend0_pchain ;
sysml:ownedRelatedElement expr:P__Car___402_pend0_pchain .The same chain feature carries a chain target a head relationship states
(redefines a.b, :>> a.b, references a.b): the chain's sysml:chainingFeature
property of the head names the chain element, and the materialized
Redefinition, Subsetting or ReferenceSubsetting owns it the same way
(KerML OwnedRedefinition/OwnedSubsetting/OwnedReferenceSubsetting). A chain
link that resolves to a graph element is written as its IRI, and one that does
not as {"@ref": "<name>"} in the JSON element form — a sysx:Expression typed
literal in Turtle — never a bare string; in a FeatureChaining the link is the
single-valued sysml:chainingFeature, in the derived list it is one entry.
Reading a graph back accepts three shapes of the same chain: the normative
FeatureChaining elements alone (which is what other tools write), the derived
sysml:chainingFeature list alone (what earlier releases of this one wrote),
and both together — where the ordered links disagree, the graph is refused
rather than read one way. A graph holds each triple once, so a repeated link
(n.next.next) appears once in the derived sysml:chainingFeature list, in
first-occurrence order; the ordered FeatureChaining elements carry the full
chain and are the list the reader takes. A chain reached in an expression
(attribute x = a.b.c;) is the nested sysml:FeatureChainExpression tree, and
the chain a a.b() invocation reaches is a chain feature an sysml:OwningMembership
owns (KerML OwnedFeatureChainMember), the same element an end's carries.
For a binary connector, sysml:sourceFeature and sysml:targetFeature
identify the two related features; sysml:relatedFeature remains the
collection-valued relation for every arity. These two predicates are not
written for n-ary connectors. A flow's payload remains the sysx:payload
expression, not an additional connector end. Transitions use
sysml:source and sysml:target; sourceFeature and targetFeature remain
the legacy transition spelling accepted on import.
Graphs written by earlier releases still import, including direct
sysml:references on an end, sysx:relatedFeature with sysx:endIndex,
sysx:endRole and sysx:endName, and transitions using
sysml:sourceFeature/sysml:targetFeature. When both a
ReferenceSubsetting and interim sysml:references are present, or when both
standard and legacy end shapes are present, the decoder refuses a graph whose
targets disagree. Non-name end targets
that cannot be represented as a linked feature remain sysx:Expression typed
literals, preserving the expression text rather than inventing an IRI.
An end written behind a multiplicity (connect [1] a to [0..1] b,
bind [0..1] a = [0..1] b) carries its bounds on that node as sysml:lowerBound
and sysml:upperBound expression nodes, the way a feature carries its own; the
decoder writes them back ahead of the end. A binding's two ends are two such
nodes like a succession's or a connector's — bind a = b relates end0 for a
and end1 for b — and neither is the connector's sysml:value: a binding
states no value and no sysml:ownedReferenceSubsetting of its own
(export_test.go:TestBindingEndMultiplicitiesAreStatedAsStructure,
binding_connector_ends_test.go). An end that
declares a name of its own and reference-subsets the feature it attaches to
(connect bead ::> t.bead to …, KerML connector a ::> a.x to b;,
bind e1 ::> a = e2 references b;) relates that
feature — the end's ReferenceSubsetting names t.bead, a chain feature when
the target is qualified, not bead —
and carries the name as sysml:declaredName and sysml:name on the same node, with
sysx:endReferencesKeyword "references" where the source spelled the word; the
decoder writes it back as <name> ::> <feature> unless that spelling is recorded
(export_test.go:TestKerMLBinaryConnectorEndsCarryTheRoundTripWithoutSourceText,
binding_connector_ends_test.go:TestKerMLBindingConnectorEndsCarryTheRoundTripWithoutSourceText).
A named end that relates no feature, or one the graph names twice, is refused as
that connector end rather than written (TestBindingEndsWithoutANotationAreRefused).
A KerML binary connector without from starts with its first end, so
connector eng to t; is an anonymous connector whose first end is eng, and a
named one writes from as its sysx:endVerb. The forms and what each writes:
sysx:endForm |
Notation | Head |
|---|---|---|
to |
<end0> to <end1, …> |
connect a to b, allocate a to b, connector c from a to b |
nary |
(<end0>, <end1>, …) |
connect (a, b, c) |
equals |
<end0> = <end1> |
bind a = b, bind e1 ::> a = e2 references b, binding [1] of a = b, binding of e1 ::> a = e2 ::> b |
firstThen |
<end0> then <end1> |
succession first a then b, succession [n] first a then b |
fromTo |
[of <payload>] from <end0> to <end1> |
flow of P from a to b |
flowTo |
[of <payload>] <end0> to <end1> |
flow a to b |
satisfy |
<requirement> (the sysml:subsets end, written bare) |
satisfy R by v, verify R |
then |
the source end is the nearest feature written before it that is not a connector or a transition; a member that is not a feature is read past | then b;, then part b; |
A head whose own keyword is the noun form writes a verb ahead of its ends, and
that verb is sysx:endVerb (connection c connect a to b). Where the keyword
is a synonym for the kind (verify for a satisfy, allocate for an
allocation) it is carried as sysx:declaredKeyword, as elsewhere.
An anonymous connector's own multiplicity (sysml:lowerBound/sysml:upperBound
on the connector, as against on an end node) is its declaration, and is written
ahead of the ends: succession [n] first a then b, binding [1] of a = b. A
declaration is always followed by the end verb, since binding [1] a = b reads
the leading [1] as the first end's multiplicity in both notations: a binding
or succession that declares something but recorded no sysx:endVerb is written
with KerML of/first or SysML bind/first, which the second hop then records
as its verb. SysML's bind shorthand declares nothing, so a bind whose graph
states a multiplicity is written binding [1] bind a = b. The all modifier
(sysml:isAll) is not a declaration either: succession all a then b writes
its ends bare, as the library does, and only a recorded sysx:endVerb puts
first after all.
The form is only recorded when rebuilding from it reproduces the head's tokens.
The encoder writes the ends back from sysx:endForm and compares them with the
source, whitespace and comments aside, before recording it — a head written over
several lines, or with a note inside it, records its form like any other
(export_test.go:TestEndFormsSurviveIrregularLayout) — so a head this mapping
cannot rebuild carries no form and stays readable as text alone. Those are the heads that say
more than their ends: an end that redefines, an inline payload declaration
(flow of x : P from a to b), or a satisfy that declares a name of its own
(satisfy s : R by v).
Converting such an element from a graph that carries no sysx:sourceText is
reported, not guessed. A graph that relates ends but gives no form at all is
reported the same way (export_test.go:TestEndsWithoutTheirFormAreReported).
The body of such a head is mapped like any other body. sysx:sourceText
carries the head's own lines and sysx:sourceTail the closing ones, as for any
member with a body (see Source text), and the members written in the
body (interface seam connect w.outp to r.inp { attribute coupling : C = C::x; })
are elements of their own, owned through sysml:ownedMember,
sysml:ownedFeature and their membership with a sysx:memberIndex, with
sysx:hasBody stating that a body was written. The same holds for the body an
action's first a then b { … } or then b { … } carries. The decoder writes
the body from those members whether or not the graph carries the head's text.
Tests: export_test.go:TestEndBindingHeadsComeBackFromTheGraphAlone,
TestEndBindingBodiesComeBackFromTheGraphAlone and
TestBehavioralHeadsComeBackFromTheGraphAlone strip sysx:sourceText from the
graph, write the notation back from the mapping alone, and convert it again.
The second graph must equal the first up to the text triples, which is what
proves the second hop loses nothing. TestBindingEndsAreStatedAsStructure covers the ends themselves.
A succession carries its two ends. Every succession is one node naming
the members it sequences, whether it was written as its own member
(succession first a then b;) or attached to one (then action b : B;, which
the parser desugars to the same edge). Its ends are the two sysml:ReferenceUsage
elements an sysml:EndFeatureMembership each owns (SysML.xtext TargetSuccession):
the then form's first end is the empty source end — a ReferenceUsage with
sysml:isEnd and no ReferenceSubsetting, since the notation names no feature
for it — and the second reference-subsets the member the then sequences to,
so the order a model declares survives
the hop:
elmt:P__Move___402
a sysml:SuccessionAsUsage ;
sysx:endForm "then" ;
sysml:targetFeature elmt:P__Move__c ;
sysx:sourceMember elmt:P__Move__a .A then that names neither end writes sysx:sourceMember and
sysx:targetMember instead, pointing at the members it sequences, since the
notation gives them no name. That is what carries a then beside a member
the notation leaves unnamed (then send Show(x) to screen;, a state's
entry; then s1;), the shape the parser used to warn about
(unnamed-succession-end) and the encoder used to refuse. Both ends are
positions in one body, so writing them back is exact: the source end is the
member before the succession, and a target that is that preceding member is
the declaration the then was written ahead of.
The member a then sequences from is the one the parser gives it: the nearest
feature before it that is not a connector or a transition. A member that is not
a feature — a doc, a comment, a rep, an import, an alias, a nested
definition or package, a multiplicity declaration, a state's defer — declares
nothing a succession can run from, so a then written after one is read past it. A connector of any kind, named or not
(connect p to q;, interface i connect …, allocate, bind, flow,
succession), and a transition relate other members rather than declaring one,
so those are read past too, while an attribute, a part, an action, a
metadata usage or any other feature is the source. This is the pilot
implementation's rule (UsageUtil.getPreviousFeature, which walks back over
every owned member that is not a Feature). Skipping the non-feature members is
also the literal reading of SysML v2 §7.17.4, which describes the source as
"the nearest occurrence lexically previous to the then, skipping over any
intervening non-occurrence usages" — a doc or an import is not a usage at
all. The connector part of the rule follows the pilot where that text is
underdetermined: read literally it would sequence from a connection (an
occurrence usage) and read past an attribute (not one), the opposite of the
pilot on both counts, and §8.3.13.6 SuccessionAsUsage states no constraint for
the implied source (OMG issue SYSML21-171 records the omission). Two parts of
the pilot's rule are not followed: the pilot sequences from a flow or
message written with no ends (message m;), which this implementation reads
past like any other connector, and it resolves an alias of a feature to that
feature, where this implementation reads past the alias as §7.17.4 does — both
known gaps. The writer folds a succession back into then by the same rule,
shared with the parser as ast.IsSuccessionSource (over ast.UsageKind.IsEdge
for the connector kinds), so action a; flow from a.x to b.x; then action b;,
action a; connect p to q; then action b; and action a; doc /* */ then action b; come back as written. The
source end is compared as the name the member answers to, which is what the
parser records: a first a then b; sequences from a, and a perform walk;
or action redefines walk; that declares no name of its own answers to walk
(KerML 7.3.4.5). A graph describing a position the notation cannot express —
sequencing from an earlier member, or from the connector, documentation or
definition the then is read past — is reported rather than written back
somewhere else (export_test.go:TestUnnamedSuccessionEndComesBackFromTheGraph,
TestHalfNamedSuccessionInAGraphIsReported,
behavior_test.go:TestThenComesBackPastTheMembersTheParserSkips,
TestThenIsRefusedWhenTheGraphSequencesFromAnotherMember,
TestThenIsRefusedWhenTheGraphSequencesFromANonFeature).
Every body that can carry a succession (definition, usage, action, state,
including a parallel state's regions, calculation and requirement) reads these
forms back as the same node, and on the fixtures a second conversion writes the
same Turtle byte for byte (export_test.go:TestSuccessionRoundTripsInEveryBody).
That is a statement about the fixtures, not the mapping: over the example corpus
the second hop reproduces the graph for all 303 files that convert, but from
the source text they carry, which the corpus gate does not strip
(rdf-corpus-roundtrip.md). An end
whose name needs quotes (first a then 'drive vehicle';) is a reference to the
element like any other; the writer quotes the name as the notation requires.
Conditions convert as their notation. The members that express
a condition are carried, each as the sysx: metaclass named above with its
condition as sysx:condition: a constraint body's conditions (assert,
assume, a bare condition, and the not of assert not … as
sysml:isNegated), a nested assert constraint [name] { … }, a requirement's
assume/require members in all three forms (an expression, the constraint
they name, or a body) together with the declaration of the constraint usage they
own — sysml:declaredName, its specializations, sysml:lowerBound/upperBound
and sysml:value with its default/:= operator (require #Goal constraint braked [1] = true;) — and
subject s : X; as the sysml:SubjectMembership it declares. The assert prefixing a named usage
(assert constraint c : C) is carried as sysx:declaredPrefix. The conditions
themselves are notation, with the limits stated above. The keyword-less condition
that closes a body is written bare, as a result expression
is, because a name alone before a ; (ready;) declares a kind-less feature rather
than referring to one — so require constraint { ready }, assert constraint { not x }
and inv { a and b } come back from the graph alone with the reference their
sysml:FeatureReferenceExpression states; a condition others follow keeps its ;
(condition_references fixture, condition_references_test.go). An assume/require
member's sysx:declaredKeyword, when present, is constraint; any other value
is reported rather than the member written in a form the keyword did not state.
A member is written in one of these forms, so a graph stating an inline
sysx:condition together with facts of another form — a constraint keyword,
a body, a sysml:references, a name, specializations, a multiplicity or a
value — is reported rather than the condition written and the rest dropped.
The nodes in an action or state body are mapped under Behavior, together with the shapes still refused there.
A synonym keyword on a declaration with no name of its own is carried like a
named one. feature :>> x;, composite :>> e = v;, snapshot :>> start { … },
timeslice :>> portionOfLife { … }, event m.start;, event occurrence e;,
assert constraint { … }, assert c { … } and assert not c; all come back from
the graph alone: the portion, the event and the assertion are typed
(sysml:portionKind, sysml:EventOccurrenceUsage, sysml:AssertConstraintUsage
with sysml:isNegated), the occurrence or constraint an event m.start or
assert c names is its sysml:references, and KerML's feature is
sysx:declaredKeyword — see What each element carries
for how the decoder chooses the spelling. Reading back, the head is spelled from
the typed facts: snapshot/timeslice from the portion kind, event from the
metaclass or sysml:isEvent, assert from the metaclass with not from
sysml:isNegated, each as the kind keyword itself where sysx:declaredKeyword
says it was written so and as a modifier ahead of occurrence/constraint
otherwise. What is still refused is a keyword that takes a reference in place
of a name — perform, exhibit, a state's entry/do/exit, event,
assert — in a shape the notation cannot state. With neither a
sysml:declaredName nor a sysml:references the graph has nothing to put in
either of the keyword's two places, perform a and perform action a. With a
sysml:declaredName under perform, exhibit, entry/do/exit or event
the name has no place at all: event e; names the e it refers to, and the
declaration is spelled event occurrence e;, which the graph does not state.
Under assert a name is read only where a typing, specialization, references
clause or value follows it (assert safe : Safe;, which the parser reads as a
declaration), so a named assertion that nothing but a body or a multiplicity
follows — assert c { … }, assert c[1] — is refused rather than written as a
reference to a different constraint. The parser never produces these shapes
(perform; declares a feature named perform), so only a graph from another
tool, or one edited by hand, states them; the decoder's refusal names the
keyword and the fact at odds.
A metadata annotation is carried structurally, as described under What
each element carries: its type, its about
targets, the sigil it was written with and its body's members as owned members
with their sysml:value expression trees, so @Safety { level = 2; } and
#Safety part def Car; come back from the graph alone. Four shapes are reported
rather than written, and only a graph from another tool can state them: a
metadata usage whose one sysml:type is a subject of another metaclass (a
sysml:PartDefinition, say — a literal type names an element the graph does not
define, so it is written as it is), a #
prefix carrying a name, an about clause or a body (the grammar's
PrefixMetadataUsage is the type alone, so the parser never produces one), a
# prefix owned by an element whose head has no prefix position (a state's
entry action, say), and a # prefix on a head kept as sysx:sourceText
(#Safety connect x to y;) whose text does not write it. @Safety part def Car; is not a prefix in the grammar —
@ introduces a member of its own, so the parser reports the missing ; or
{ after @Safety — and it is refused at the parser, before conversion.
A name declared twice in one namespace is refused. An element's derived id
is the encoding of its qualified name, so part def A; part def A; in one
container would merge into a single subject. The duplicate is reported instead.
A shorthand relationship declares no name of its own: the result in bind result = x;
and the x in first x; name the end the statement relates. Those elements are
therefore addressed by position (sysx:memberIndex) and the name is carried as a
reference. Without that they would collide with the member they name and the model
would be refused as a duplicate.
Unsupported on the RDF input side, each reported as an error naming the line or element:
- blank nodes and
[ ... ]— every element must have a stable IRI - RDF collections
( ... )— order is carried bysysx:memberIndex - an element with no
rdf:type, or a metaclass outside the mapping, or severalrdf:types none of which is a subclass of all the others - a reference whose IRI names no subject of the graph and whose id no subject
carries as
sysml:elementId; a dangling id is reported as such, never left as a silently unresolvable name - a referenced element with no
sysml:qualifiedName; the name is read from that property, never recovered from the IRI, so a graph with foreign ids (UUIDs, say) converts exactly as long as it carries the names - an element whose
sysml:owningNamespaceis not in the graph - ownership that forms a cycle, leaving an element no root owns; printing walks down from the roots, so this would otherwise write an empty document
- Turtle syntax errors, reported with a line number
- literal shorthands (bare numbers and booleans); literals must be quoted,
with an
xsd:datatype where one applies - a literal whose datatype its property does not take, or with a language tag.
Every metamodel property the mapping reads as text is a
String, so a name is a plain orxsd:stringliteral;"3"^^xsd:integeror"x"@enstated as one is a different term, not the name3orx, and is reported naming the literal and the subject that states it. The other properties take the datatypes the ontology gives them, so a plain string is refused there too:xsd:booleanfor the flags andsysx:hasBody;xsd:integerorxsd:intfor thesysx:indexes and the bounds; and for thesysml:valueof a literal expression, by its class,xsd:integerorxsd:int(LiteralInteger),xsd:decimal,owl:real,xsd:doubleorxsd:float(LiteralRational),xsd:boolean(LiteralBoolean) or a string (LiteralString). Asysx:Expressionliteral is taken only where the mapping writes notation — a relationship target — never as a name - a literal whose text is outside its datatype's lexical space
(
"false"^^xsd:int,"yes"^^xsd:boolean,"1e3"^^xsd:decimal): it is no term of that datatype, so it is reported rather than read as the text it spells, as is anxsd:intoutside its 32-bit value space.owl:real, which names no lexical forms of its own, takes a finitexsd:double's - a current
sysx:memberIndexthat is negative or too large for the platform'sint: it is a position the writer orders by, and one it cannot hold would otherwise be read as 0 and move the member to the front. The oldersysx:argumentIndex,sysx:endIndex,sysx:endRoleandsysx:endNameterms are legacy-only and are no longer written; currentconnectorEndandownedFeatureMembershiporder comes from theirjson:collection annotations - a subject stating a single-valued property twice with different objects —
a body with two
sysx:resultExpressions, an element with twosysx:memberIndexes, twosysx:isNamespaceImportflags or asysml:isDefaultstated both true and false: only one could be written, so the graph is refused naming both rather than the first being kept. Everysysx:property is single-valued but the members and parameters of a body,sysx:deferredEventandsysx:prefixMetadata; the legacysysx:relatedFeatureend collection is no longer written; of thesysml:properties, the booleanis…flags andsysml:portionKindare. A triple stated twice is one triple to the graph, so only differing objects are a conflict - a
sysml:isDefaultorsysml:isInitial, whether true or false, on a subject with nosysml:value: the flags spell the operator a feature value is written with (default =,:=), so without a value there is nothing to write them on
A graph that uses none of OpenSysML's sysx: properties (one produced by
another tool) converts as far as the mapping allows and errors on the first
element it cannot place, rather than emitting a model with elements missing.
sysml -convert api-json (%save model.json, the service's Convert to or from
api-json or json) writes the graph above in the form the OMG SysML v2 API &
Services specification serves from its /elements endpoints: a JSON array of
element objects, one per graph subject in graph order, each with the metaclass as
@type, the element id as @id, and the metamodel's properties as keys. It is
not a second mapping. The Turtle path and this one build one graph (ToRDF) and
read back into one graph (ToSysML), so everything the sections above say about
metaclasses, identity, expressions, collections and limitations holds here
unchanged, and ttl ↔ api-json converts through the graph without touching
notation.
[
{
"@type": "PartDefinition",
"@id": "Vehicles__Wheel",
"qualifiedName": "Vehicles::Wheel",
"elementId": "Vehicles__Wheel",
"sysx:memberIndex": 0,
"owningNamespace": { "@id": "Vehicles" },
"owner": { "@id": "Vehicles" },
"owningRelationship": { "@id": "Vehicles__Wheel_om" },
"owningMembership": { "@id": "Vehicles__Wheel_om" },
"declaredName": "Wheel",
"sysx:hasBody": true,
"ownedMember": [
{ "@id": "Vehicles__Wheel__diameter" },
{ "@id": "Vehicles__Wheel__mass" }
],
"sysx:sourceText": " part def Wheel {\n",
"sysx:sourceTail": " }\n"
}
]How each part of the graph is spelled:
| Graph | Element form |
|---|---|
rdf:type sysml:PartDefinition |
"@type": "PartDefinition"; a metaclass of this project's own (sysx:Pseudostate, …) keeps its prefix, "@type": "sysx:Pseudostate" |
| The subject's IRI | "@id": the id after the final : of an elmt: or expr: IRI, with its project qualifier where the IRI has one (Interop:Vehicles__Wheel) — the same spelling the collection annotations use |
A sysml: property |
the bare property name as key, in triple order |
A sysx: property |
"sysx:<name>" as key |
The urn:sysmlv2:annotation:json: annotation of a collection |
nothing of its own — it decides that the property it annotates is an array |
| An IRI object | {"@id": <id>}, the id spelled from the subject as above |
An xsd:boolean, xsd:integer, xsd:decimal/xsd:double literal |
a JSON boolean or number; a real whose lexical form JSON cannot spell is given the digits it needs (.1 → 0.1, 5. → 5.0), and INF or NaN is refused; a literal in another datatype (xsd:float, xsd:int, owl:real, a xsd:double without an exponent) is refused, since the form carries no datatype and the reader would restore a different one |
| A plain literal (and, on the properties that carry it, expression text) | a JSON string; a literal in any other datatype is refused |
| A name literal on an object property | {"@ref": <name>} — sysml-toolkit's spelling of a target the writer could not resolve, read back as the same name literal |
A sysml: property the metamodel declares multi-valued (unbounded upper) |
an array of however many values the graph states, in the order the annotation records or in triple order when no annotation states it |
A sysml: property stated more than once with no annotation, that the metamodel declares single-valued |
refused, since the graph does not say which order the values have |
A sysx: property stated more than once |
an array, in triple order |
Reading is the inverse, and refuses rather than guesses: the document is one
element object or an array of them; every object carries a non-empty @id and a
@type, no @id occurs twice, no @ key other than those two is accepted, no key
or @type is in a prefix other than the bare sysml: names and sysx:, an object
value is a reference {"@id": …} or a {"@ref": <name>} — sysml-toolkit's
spelling of a target it could not resolve, which reads as the name literal the
mapping writes for a name-valued reference — and an array holds no array and no
null. A null value states no triple. An array on a sysml: property becomes
the repeated triples and the collection annotation the Turtle path would have
written, so the graph read from the JSON form is the graph the Turtle form parses
to, triple for triple. An @id is resolved within the subject's project scope, or
by the <qualifier>:<id> it spells; an id in the expression grammar
(Expressions) whose parent is a document element, carrying no
qualifiedName, is an expr: node when its metaclass is one the mapping mints
directly under a declaration — an expression class, the end feature of a
connector, or a reference subsetting — or when its parent is itself an expr:
node or an expression-class element. A membership id
(_om) follows the node it owns, and any other element spelled that way is an
ordinary element. The form carries no namespace of its own, so this is a reading
of the id.
The pilot implementation serializes every document as an unnamed top-level
Namespace with no owningRelationship, whose OwningMemberships own the
document's top-level packages (the head of any .kermlx). The element form
writes the same: an unnamed sysml:Namespace first in the array, one
OwningMembership per top-level element, and each top-level element's
owningRelationship, owningMembership, owningNamespace and owner
pointing back at them. The ids are derived, never declared, so they are the
same on every run: in the qualified form the namespace is the first
top-level element's id with _ns appended and each membership the member's
id with _om, the suffix every other owning membership uses; in the uuid
form both are UUIDv5 names in the same namespace the top-level element's
uuid is minted in, so a document's ids are stable across the two forms and
across re-exports. Encoded names cannot spell either suffix, but a document
read from another writer may already use any id, so a suffix is repeated
(P_ns_ns, P_om_om) until it names a subject the graph does not hold; the
choice depends only on the graph, so it too is the same on every run.
The Turtle form does not carry the wrapper. Turtle is this mapping's own
notation round-trip carrier, and its root subjects are the document's own
packages; adding a subject the notation has no spelling for would change
every .ttl golden and the corpus ratchet for no reader that wants it. The
identity gate over the pilot library (tests/identity) pins the ids of
named library elements, which the wrapper does not touch. The reader treats
the wrapper as transparent in both directions: an unnamed, unowned
Namespace whose only content is OwningMemberships of top-level elements
is stripped before the graph converts to notation, whether the toolkit or
this mapping wrote it, so notation → api-json → notation is byte-identical.
A root namespace that carries a name, an owner or members of its own is an
ordinary element and is kept.
Two readings are decided by the graph rather than the JSON, and are worth knowing:
- Multi-valued properties are arrays; single-valued ones are objects or
scalars. The element form's property shape follows the metamodel's upper
multiplicity (
SysML.ecoreupperBound="-1"in the generatedinternal/translate/rdf/ontologytable):ownedMemberwith one value is[{"@id": …}],owningRelationshipis{"@id": …}. Thejson:annotation still fixes member order where the graph states it. The reader accepts both shapes and records the annotation only for two members or more, so a one-member array reads back to the same graph Turtle produces. What the SysML v2 API's own commit path serves back for the elements this form posts is measured, not assumed: the opt-inTestFlexoInteropharness posts them asDataVersionpayloads and reports the elements and properties the service returns beside those the Turtle graph-load path returns (the same set on the reference fixture, thesysx:properties excepted, which the service drops on both paths). - A string on a reference-valued property is a name or an expression. The
encoder writes an unresolved target as its name (
"type": "Real") and a computed one as expression text; JSON has one string for both. On the properties the encoder writes expression text on (type,general,memberElement, the connector ends and their subsetting) a string that does not parse as a name is expression text; on every other property it is a name.
Interchange with sysml-toolkit (the Open-MBEE Rust toolkit) runs both ways.
The mapping materializes the same relationship elements the toolkit emits
(FeatureTyping, Subclassification, Redefinition, Subsetting,
ReferenceSubsetting, MultiplicityRange,
ConjugatedPortDefinition/PortConjugation, the ParameterMembership,
FeatureValue and ReturnParameterMembership structure of every expression,
the Membership an expression referent is carried by — the table above) and
writes the same unnamed root Namespace with its OwningMembership (see
The root namespace), so the toolkit's lifter reads this
form's output back to the notation it came from. The per-@type element
counts of the two still differ where the toolkit's own JSON is sparser than
the metamodel: it writes no ReturnParameterMembership for an expression
whose result is unused, spells a chained callee as FeatureChainings on one
feature where this mapping owns a FeatureChainExpression per link, and
writes a sysml:CaseDefinition where an analysis def is an
AnalysisCaseDefinition. What the toolkit does not
read are the sysx: extension metaclasses listed above, and one place where
its lifter is narrower than the metamodel and this mapping keeps the spec's
shape: the toolkit names a referent only by qualified name, so a referent
that is an anonymous owned element prints as that element's id with a
cannot name reference target warning. That is a body argument
(cars->select { in c; c.mass > m }), which is, as in the pilot's XMI, a
FeatureReferenceExpression whose referent is the anonymous Expression it
owns through a FeatureMembership; and a connector end written as a chain
(connect lv.payload to cm.dock), whose end feature subsets the unnamed
Feature owning the FeatureChainings (KerML 1.0 § 8.3.3.3.5
FeatureChaining), so the toolkit prints end ref ::> <id> instead of the chain.
The toolkit's own JSON — convert --to compact-json or --to full-json —
reads into the same graph through ReadAPIJSON and converts to notation like
any graph this mapping holds:
- Both forms decode identically.
full-jsonstates every property of an element the compact form collapses onto its owner (typeon a usage for theFeatureTypingelement,isImpliedIncluded, the membership ends), so the reader derives the collapsed spelling back: asysml:FeatureTypingstating nothing the collapsedtypeedge does not already say contributes its ends and is then elementless, anisImpliedIncludedelement drops the stated defaults (isEnd,isReference,mayTimeVary, …) its owner's spelling never writes, and the resulting graph is the graph a compact document produces — compact and full of one model convert to byte-identical notation. - The root
Namespaceis transparent. The toolkit wraps the document in a rootNamespaceand oneOwningMembership; the reader strips both, so apackage P { … }comes back without a synthetic wrapper. - A
{"@ref": <name>}target is the unresolved name it spells, matching the name literal this form writes for the same case, and the dangling IRI a full document uses for the same target (unresolved:-derived, recoverable from the reference'sx-sysmlv2-unresolved-referencetextual annotation) reads as the same name. - Ends the notation alone cannot place are refused. Where a member's two
collapsed ends disagree, or an element names no owner it can sit under,
-convert sysmlfails rather than guesses a position. - uuid ids are derived, not declared. A document whose
@ids are the-id uuidform's name-based uuids reads back to the same notation, the ids implied as in the default form; only an id that does not match the derivation stays a declared@ElementIdannotation.
The per-file ratchet over examples/ runs for this form too:
TestCorpusAPIJSONRoundTrip in tests/corpus/roundtrip_test.go converts each
model notation → api-json → notation → api-json and pins the verdict in
testdata/api_json_roundtrip_expected.txt
(rdf-corpus-roundtrip.md). Its verdicts
are the Turtle gate's for every file but two, whose .1 reals JSON respells as
0.1 (graph-diff).
| Package | Role |
|---|---|
internal/translate/rdf |
Triple/graph model, Turtle writer, Turtle parser |
internal/translate/export |
ToRDF (AST → graph), ToSysML (graph → notation), WriteAPIJSON/ReadAPIJSON (graph ↔ the API element form) |
internal/translate/convert |
The Convert entry point: format names, notation parsing, the SysML v1 migration |
tests/corpus/roundtrip_test.go |
The per-file round-trip ratchets over every model under examples/, with their baselines in testdata/corpus_roundtrip_expected.txt (Turtle) and testdata/api_json_roundtrip_expected.txt (the API element form) (rdf-corpus-roundtrip.md) |
internal/frontend/repl |
%save |
cmd/sysml |
-convert, -from, -o |
The RDF layer is hand-written against the Turtle grammar rather than pulled in as a dependency: the subset needed here is small, and the parser rejects what it does not support instead of accepting it and dropping data.