From 4b3e9b1855fe4ca52dcb0226037fbb94d9e06f16 Mon Sep 17 00:00:00 2001 From: Somesh Kashyap Date: Wed, 30 Sep 2026 07:12:12 -0400 Subject: [PATCH 1/2] feat(export): name the standard library elements a converted model references (#731) A reference to a library element is its normative id, a hash no reader can turn back into a name. Each referenced library element is now stated under that id with its metaclass, elementId, qualifiedName and declaredName, marked isLibraryElement; a referenced library membership with its memberElement. The library itself is not exported. Reading back, such an element is a reference into the bundled library, checked against it, and not written; repository sync leaves it alone. Co-Authored-By: Claude Opus 5.5 --- .../unreleased/library-element-names.added.md | 1 + docs/reference/rdf-mapping.md | 23 +++ .../translate/export/api_json_import_test.go | 5 + internal/translate/export/behavior.go | 8 +- internal/translate/export/rdf_in.go | 53 ++++++- internal/translate/export/rdf_out.go | 91 ++++++++++++ internal/translate/export/root_namespace.go | 13 +- internal/translate/interop/reposync/diff.go | 11 +- tests/export/export_test.go | 5 + tests/export/library_names_test.go | 140 ++++++++++++++++++ .../testdata/convert/action_nodes.golden.ttl | 14 ++ .../convert/anonymous_portions.golden.ttl | 21 +++ .../convert/condition_references.golden.ttl | 21 +++ .../testdata/convert/imports.golden.ttl | 20 +++ .../convert/invariant_references.golden.ttl | 21 +++ .../convert/irregular_layout.golden.ttl | 14 ++ .../testdata/convert/loop_scopes.golden.ttl | 7 + .../export/testdata/convert/parts.golden.ttl | 7 + .../convert/quoted_succession_ends.golden.ttl | 14 ++ .../relationship_clause_order.golden.ttl | 14 ++ .../convert/shadowing_body.golden.ttl | 14 ++ .../convert/then_after_members.golden.ttl | 28 ++++ .../convert/views_flows_parameters.golden.ttl | 7 + 23 files changed, 545 insertions(+), 7 deletions(-) create mode 100644 changes/unreleased/library-element-names.added.md create mode 100644 tests/export/library_names_test.go diff --git a/changes/unreleased/library-element-names.added.md b/changes/unreleased/library-element-names.added.md new file mode 100644 index 000000000..30ae10acc --- /dev/null +++ b/changes/unreleased/library-element-names.added.md @@ -0,0 +1 @@ +- **A converted model names the standard library elements it references.** A reference to a library element is its normative id, a hash no reader can turn back into a name. Turtle, the API's JSON and `Convert` now state each referenced library element under that id, with its metaclass, `qualifiedName` and `declaredName`, marked `isLibraryElement` (KerML `Element::isLibraryElement`). Every `@id` an API JSON output references is then an element of it. The library itself is still not exported, and reading a graph back checks each name against the bundled library (#731). diff --git a/docs/reference/rdf-mapping.md b/docs/reference/rdf-mapping.md index 5bf1561db..dc9446edf 100644 --- a/docs/reference/rdf-mapping.md +++ b/docs/reference/rdf-mapping.md @@ -250,6 +250,29 @@ 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. +**A converted model names the library elements it references.** A normative +id is a hash of a name, and cannot be turned back into one. So for each standard +library element the output references, the writer states the element under its +normative id, marked `sysml:isLibraryElement true` (KerML +`Element::isLibraryElement`, `Element_isLibraryElement` in the ontology), with: +- its `rdf:type` (`LibraryPackage` for a library package); +- `sysml:elementId`, `sysml:qualifiedName` and `sysml:declaredName`. + +Nothing else is written about it: its owner, members and relationships stay in +the library, and the library itself is not exported. A library membership the +output references (the `importedMembership` of `import ScalarValues::Real;`) is +stated as a `sysml:OwningMembership` with its `sysml:memberElement`, marked the +same way. + +So every `@id` an API JSON output references is an element of it. Such an +element has no owner in the graph, and the document's root namespace does not +take it (`LibraryReference`). + +Reading a graph back, such an element is a reference into the bundled library, +not a declaration, and it is not written. A qualified name other than the one +the library gives its id is refused rather than trusted, and so is an id the +library does not have. Repository sync never creates, changes or deletes one. + **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 diff --git a/internal/translate/export/api_json_import_test.go b/internal/translate/export/api_json_import_test.go index ca024be5c..80b6be0e6 100644 --- a/internal/translate/export/api_json_import_test.go +++ b/internal/translate/export/api_json_import_test.go @@ -47,6 +47,11 @@ func countTable(t *testing.T, data []byte) map[string]int { } table := map[string]int{} for _, object := range objects { + // A library element the export names (isLibraryElement) is a reference + // into the library, not an element of the model. + if object["isLibraryElement"] == true { + continue + } if typ, ok := object["@type"].(string); ok { table[typ]++ } diff --git a/internal/translate/export/behavior.go b/internal/translate/export/behavior.go index 71ffdd7f0..a8785a54c 100644 --- a/internal/translate/export/behavior.go +++ b/internal/translate/export/behavior.go @@ -87,7 +87,7 @@ var libraryDone = ast.QualifiedNameOf("Actions", "Action", "done") // libraryReference is the subject of a standard library element named from // the global scope, or its name when no library is loaded. -func (e *encoder) libraryReference(name *ast.QualifiedName) rdf.Term { +func (e *encoder) LibraryReference(name *ast.QualifiedName) rdf.Term { if decl, fqn, ok := e.linked(e.res.ResolveQualified(nil, name)); ok { return e.ids.subjectForNode(decl, fqn) } @@ -138,7 +138,7 @@ func (e *encoder) encodeBehavior(node ast.Node, head func(rdf.Term), subject rdf // `done;` is a Membership of the library's Actions::Action::done. head(rdf.SysMLTerm(mMembership)) e.graph.Add(subject, e.sysx(xDeclaredKeyword), rdf.String("done")) - e.graph.Add(subject, e.sysml(pMemberElement), e.libraryReference(libraryDone)) + e.graph.Add(subject, e.sysml(pMemberElement), e.LibraryReference(libraryDone)) return true, nil case *ast.ForkNode: @@ -232,7 +232,7 @@ func (e *encoder) encodeBehavior(node ast.Node, head func(rdf.Term), subject rdf } else if _, done := n.TargetMember.(*ast.FinalNode); done { // `then done;` targets the library's Actions::Action::done, the // same end `succession first x then done;` states outright. - target.targetTerm = e.libraryReference(libraryDone) + target.targetTerm = e.LibraryReference(libraryDone) } else if n.TargetMember != nil { if fqn, ok := e.fqn[n.TargetMember]; ok { target.targetTerm = e.ids.subjectForNode(n.TargetMember, fqn) @@ -672,7 +672,7 @@ func (e *encoder) edgeEnds(subject rdf.Term, node ast.Node, owner string, src, t if _, done := end.end.member.(*ast.FinalNode); done { // `then done;` reaches the library's Actions::Action::done // feature, which an explicit succession end names outright. - e.graph.Add(subject, e.sysml(end.feature), e.libraryReference(libraryDone)) + e.graph.Add(subject, e.sysml(end.feature), e.LibraryReference(libraryDone)) continue } fqn, ok := e.fqn[end.end.member] diff --git a/internal/translate/export/rdf_in.go b/internal/translate/export/rdf_in.go index 0bf2f3a91..b92f3c647 100644 --- a/internal/translate/export/rdf_in.go +++ b/internal/translate/export/rdf_in.go @@ -731,7 +731,14 @@ func (d *decoder) build() ([]*element, error) { order []*element roots []*element ) + stubs, err := d.libraryStubs() + if err != nil { + return nil, err + } for _, subject := range d.graph.Subjects() { + if stubs[subject.Value] { + continue + } if d.isMembership(subject) { if err := d.readMembership(subject); err != nil { return nil, err @@ -739,7 +746,7 @@ func (d *decoder) build() ([]*element, error) { } } for _, subject := range d.graph.Subjects() { - if d.isMembership(subject) || d.isExpressionNode(subject) || d.isFilterPackageNode(subject) { + if stubs[subject.Value] || d.isMembership(subject) || d.isExpressionNode(subject) || d.isFilterPackageNode(subject) { // A node of an expression graph belongs to the declaration that holds // the expression, not to an element of its own; so does a filter package. continue @@ -1219,6 +1226,50 @@ func (d *decoder) referencedElement(iri string) (*element, error) { return target, nil } +// libraryStubs finds the subjects that name a standard library element the +// graph references (sysml:isLibraryElement, and no owner in the graph): they +// are references into the bundled library, not declarations, so they are not +// written back. Each one's qualified name must be the one the library gives its +// id, and a membership one must own such an element; a stub the library does +// not know, or names otherwise, is refused rather than trusted. +func (d *decoder) libraryStubs() (map[string]bool, error) { + stubs := map[string]bool{} + for _, subject := range d.graph.Subjects() { + if !LibraryReference(d.graph, subject) { + continue + } + id := rdf.LocalName(subject.Value) + if stated, ok := d.graph.Lexical(subject, rdf.SysML+pElementID); ok { + id = stated + } + if member, ok := d.graph.Object(subject, rdf.SysML+pMemberElement); ok { + if lexical, ok := d.graph.Lexical(member, rdf.SysML+pIsLibraryElement); !ok || lexical != "true" { + return nil, &UnsupportedError{ + What: fmt.Sprintf("the library membership <%s>", subject.Value), + Note: "its memberElement is no library element, and a library membership owns one", + } + } + stubs[subject.Value] = true + continue + } + lib, ok := d.libraryElement(subject.Value, id) + if !ok { + return nil, &UnsupportedError{ + What: fmt.Sprintf("the library element <%s>", subject.Value), + Note: fmt.Sprintf("it is marked sysml:isLibraryElement, but the bundled standard library has no element with id %q", id), + } + } + if stated, ok := d.graph.Lexical(subject, rdf.SysML+pQualifiedName); ok && stated != lib.qname { + return nil, &UnsupportedError{ + What: fmt.Sprintf("the library element <%s>", subject.Value), + Note: fmt.Sprintf("the graph names it %s, but the bundled standard library gives id %q to %s", stated, id, lib.qname), + } + } + stubs[subject.Value] = true + } + return stubs, nil +} + // libraryElement is the standard library element whose normative id a reference // names, if the norm fixes id for one; the graph need not define it. func (d *decoder) libraryElement(iri, id string) (*element, bool) { diff --git a/internal/translate/export/rdf_out.go b/internal/translate/export/rdf_out.go index d2d21cdca..93c2c83a8 100644 --- a/internal/translate/export/rdf_out.go +++ b/internal/translate/export/rdf_out.go @@ -20,6 +20,7 @@ import ( // Property names in the SysML vocabulary. const ( pDeclaredName = "declaredName" + pIsLibraryElement = "isLibraryElement" pDeclaredShortName = "declaredShortName" pQualifiedName = "qualifiedName" pElementID = "elementId" @@ -398,6 +399,7 @@ func (e *encoder) encodeDocument(root *ast.RootNamespace) error { if e.idErr != nil { return e.idErr } + e.libraryNames() e.sourceText() if err := rdf.AnnotateCollections(e.graph); err != nil { return err @@ -483,6 +485,9 @@ func newEncoderOver(file *source.SourceFile, root *ast.RootNamespace, form IDFor } type encoder struct { + // libraryRefs are the standard library elements the document links to, by + // subject IRI, so the graph can name the ones it references. + libraryRefs map[string]libraryRef // minted lists the subjects minted for this document's declarations, which a // model of several documents checks no other document declares too. minted []mintedSubject @@ -646,6 +651,84 @@ func (e *encoder) importedMembership(name *ast.QualifiedName) rdf.Term { return rdf.String(qualifiedText(name)) } +// libraryRef is a standard library element the document links to: its +// declaration, qualified name and normative subject, and the subject of its +// owning membership where the norm fixes one. +type libraryRef struct { + node ast.Node + fqn string + subject, membership rdf.Term +} + +// libraryNames states, for each standard library element the graph references, +// what the graph cannot otherwise say about it: its metaclass and qualified +// name, marked isLibraryElement (KerML Element::isLibraryElement), so a reader +// without the library can name what a normative id stands for. The library +// itself is not exported: the element's members, relationships and owner stay +// in the library. A library membership the graph references (an import of a +// library member) is stated as the OwningMembership of that element. +func (e *encoder) libraryNames() { + if len(e.libraryRefs) == 0 { + return + } + referenced := map[string]bool{} + for _, t := range e.graph.Triples() { + if t.Object.IsIRI() { + referenced[t.Object.Value] = true + } + } + subjects := make([]string, 0, len(e.libraryRefs)) + for iri := range e.libraryRefs { + subjects = append(subjects, iri) + } + slices.Sort(subjects) + named := map[string]bool{} + // name states ref's element, reporting whether it is named in the graph. + name := func(ref libraryRef) bool { + if done, seen := named[ref.subject.Value]; seen { + return done + } + named[ref.subject.Value] = false + metaclass := declaredMetaclass(ref.node) + switch n := ref.node.(type) { + case *ast.Package: + // `standard library package` (KerML 1.0 ยง 8.3.4.13.3 LibraryPackage). + metaclass = mPackage + if n.IsLibrary { + metaclass = mLibraryPackage + } + case *ast.Alias: + // An alias is a Membership, which the graph names by the membership + // it is rather than as an element. + return false + } + if metaclass == "" { + return false + } + named[ref.subject.Value] = true + e.graph.Add(ref.subject, rdf.IRI(rdf.RDFType), e.sysml(metaclass)) + e.graph.Add(ref.subject, e.sysml(pElementID), rdf.String(rdf.LocalName(ref.subject.Value))) + e.graph.Add(ref.subject, e.sysml(pQualifiedName), rdf.String(ref.fqn)) + if declared, _ := declaredNameAndMembers(ref.node); declared != "" { + e.graph.Add(ref.subject, e.sysml(pDeclaredName), rdf.String(declared)) + } + e.graph.Add(ref.subject, e.sysml(pIsLibraryElement), rdf.Bool(true)) + return true + } + for _, iri := range subjects { + ref := e.libraryRefs[iri] + if referenced[iri] { + name(ref) + } + if ref.membership.Value != "" && referenced[ref.membership.Value] && name(ref) { + e.graph.Add(ref.membership, rdf.IRI(rdf.RDFType), e.sysml(mOwningMembership)) + e.graph.Add(ref.membership, e.sysml(pElementID), rdf.String(rdf.LocalName(ref.membership.Value))) + e.graph.Add(ref.membership, e.sysml(pMemberElement), ref.subject) + e.graph.Add(ref.membership, e.sysml(pIsLibraryElement), rdf.Bool(true)) + } + } +} + // claimLibrary reserves the IRIs of a library element the document links to, // and of its owning membership, so no element declared here lands on them. func (e *encoder) claimLibrary(node ast.Node, fqn string) { @@ -656,6 +739,14 @@ func (e *encoder) claimLibrary(node ast.Node, fqn string) { e.ids.owningMembershipOf(node, subject).Value, fqn + "'s owning membership", }) } + if e.libraryRefs == nil { + e.libraryRefs = map[string]libraryRef{} + } + ref := libraryRef{node: node, fqn: fqn, subject: subject} + if e.ids.normativeMembership(node) { + ref.membership = e.ids.owningMembershipOf(node, subject) + } + e.libraryRefs[subject.Value] = ref for _, c := range claims { if prior, taken := e.claim(c.iri, c.standsFor); taken && e.idErr == nil { e.idErr = &UnsupportedError{ diff --git a/internal/translate/export/root_namespace.go b/internal/translate/export/root_namespace.go index b8836ce59..9ce0029d7 100644 --- a/internal/translate/export/root_namespace.go +++ b/internal/translate/export/root_namespace.go @@ -229,7 +229,9 @@ func unownedElements(graph *rdf.Graph) []rdf.Term { if !strings.HasPrefix(subject.Value, rdf.Element) || !strings.HasPrefix(graph.Type(subject), rdf.SysML) { continue } - if hasOwner(graph, subject) { + // A library element the graph names is owned in the library, not + // here: the document's namespace does not take it. + if hasOwner(graph, subject) || LibraryReference(graph, subject) { continue } roots = append(roots, subject) @@ -237,6 +239,15 @@ func unownedElements(graph *rdf.Graph) []rdf.Term { return roots } +// LibraryReference reports whether subject names a standard library element +// (or its owning membership) the graph references rather than declares: marked +// sysml:isLibraryElement, with no owner in the graph. +func LibraryReference(graph *rdf.Graph, subject rdf.Term) bool { + lexical, ok := graph.Lexical(subject, rdf.SysML+pIsLibraryElement) + return ok && lexical == "true" && !hasOwner(graph, subject) && + !graph.HasProperty(subject, rdf.SysML+pMembershipOwningNamespace) +} + func hasOwner(graph *rdf.Graph, subject rdf.Term) bool { return graph.HasProperty(subject, rdf.SysML+pOwner) || graph.HasProperty(subject, rdf.SysML+pOwningRelationship) || diff --git a/internal/translate/interop/reposync/diff.go b/internal/translate/interop/reposync/diff.go index b13921a64..3c6d74058 100644 --- a/internal/translate/interop/reposync/diff.go +++ b/internal/translate/interop/reposync/diff.go @@ -351,8 +351,17 @@ func viewOf(g *rdf.Graph, rep Carrier) (map[string]*subjectView, []UncarriedProp } views := map[string]*subjectView{} uncarried := map[string]int{} + // A library element the graph names (isLibraryElement) is a reference into + // the standard library, not an element of the model: it is never created, + // changed or deleted in a repository. + library := map[string]bool{} + for _, subject := range g.Subjects() { + if export.LibraryReference(g, subject) { + library[subject.Value] = true + } + } for _, triple := range g.Triples() { - if !triple.Subject.IsIRI() { + if !triple.Subject.IsIRI() || library[triple.Subject.Value] { continue } id := rdf.LocalName(triple.Subject.Value) diff --git a/tests/export/export_test.go b/tests/export/export_test.go index 74bcf33f8..8aeca1efc 100644 --- a/tests/export/export_test.go +++ b/tests/export/export_test.go @@ -3259,6 +3259,11 @@ func TestFixtureElementIDsRoundTrip(t *testing.T) { t.Fatalf("%s: %v", path, err) } for _, subject := range graph.Subjects() { + // A library element the graph names keeps the normative id the + // references to it carry; its name is checked against the library. + if export.LibraryReference(graph, subject) { + continue + } if strings.HasPrefix(subject.Value, rdf.Expression) && strings.HasSuffix(subject.Value, "_om") { if _, ok := graph.Object(subject, rdf.SysML+"memberElement"); !ok { t.Errorf("%s: expression membership %s has no member", path, subject.Value) diff --git a/tests/export/library_names_test.go b/tests/export/library_names_test.go new file mode 100644 index 000000000..9cfa7c328 --- /dev/null +++ b/tests/export/library_names_test.go @@ -0,0 +1,140 @@ +package export_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/translate/convert" +) + +const libraryNamesModel = `package L { + private import ScalarValues::Real; + private import ISQ::*; + part def Car { + attribute speed : Real; + attribute weight : MassValue; + attribute count : ScalarValues::Integer; + } +} +` + +// A converted model names each standard library element it references, marked +// isLibraryElement (KerML Element::isLibraryElement), so every @id the output +// references is an element of it; the library itself is not exported, and the +// document's root namespace does not take the library elements (#731). +func TestConvertNamesTheLibraryElementsItReferences(t *testing.T) { + out, err := convert.Convert("l.sysml", []byte(libraryNamesModel), convert.FormatSysML, convert.FormatAPIJSON) + if err != nil { + t.Fatal(err) + } + var elements []map[string]any + if err := json.Unmarshal(out, &elements); err != nil { + t.Fatal(err) + } + ids := map[string]bool{} + library := map[string]map[string]any{} + for _, e := range elements { + id, _ := e["@id"].(string) + ids[id] = true + if e["isLibraryElement"] == true { + library[id] = e + } + } + var dangling []string + var walk func(any) + walk = func(v any) { + switch v := v.(type) { + case map[string]any: + if id, ok := v["@id"].(string); ok && len(v) == 1 && !ids[id] { + dangling = append(dangling, id) + } + for _, x := range v { + walk(x) + } + case []any: + for _, x := range v { + walk(x) + } + } + } + for _, e := range elements { + walk(e) + } + if len(dangling) > 0 { + t.Errorf("the output references ids it does not hold: %v", dangling) + } + want := map[string]string{ + "ScalarValues::Real": "DataType", + "ScalarValues::Integer": "DataType", + "ISQBase::MassValue": "AttributeDefinition", + "ISQ": "LibraryPackage", + } + named := map[string]string{} + memberships := 0 + for _, e := range library { + for _, owned := range []string{"owner", "owningRelationship", "owningNamespace", "owningMembership"} { + if _, ok := e[owned]; ok { + t.Errorf("the library element %v states an owner (%s) in the graph", e["qualifiedName"], owned) + } + } + if e["@type"] == "OwningMembership" { + memberships++ + continue + } + name, _ := e["qualifiedName"].(string) + named[name], _ = e["@type"].(string) + } + for name, typ := range want { + if named[name] != typ { + t.Errorf("%s is named as %q, want a %s", name, named[name], typ) + } + } + if memberships != 1 { + t.Errorf("the membership import of Real names %d library memberships, want 1", memberships) + } +} + +// The names are a record, not declarations: the graph reads back, source text +// stripped, as the notation that produced it, in Turtle and in the API's JSON. +func TestLibraryNamesReadBackAsReferences(t *testing.T) { + _, back := graphOnlyRoundTrip(t, "l.sysml", []byte(libraryNamesModel)) + if string(back) != libraryNamesModel { + t.Errorf("the notation read back changed:\n%s", back) + } + doc, err := convert.Convert("l.sysml", []byte(libraryNamesModel), convert.FormatSysML, convert.FormatAPIJSON) + if err != nil { + t.Fatal(err) + } + fromJSON, err := convert.Convert("l.json", doc, convert.FormatAPIJSON, convert.FormatSysML) + if err != nil { + t.Fatalf("the API JSON did not read back: %v", err) + } + for _, want := range []string{"attribute speed : Real;", "attribute count : ScalarValues::Integer;"} { + if !strings.Contains(string(fromJSON), want) { + t.Errorf("the notation should contain %q:\n%s", want, fromJSON) + } + } + if strings.Contains(string(fromJSON), "datatype") || strings.Contains(string(fromJSON), "library package") { + t.Errorf("a library element was written back as a declaration:\n%s", fromJSON) + } +} + +// A library element named otherwise than the bundled library names its id is +// refused rather than trusted. +func TestLibraryNameThatDisagreesIsRefused(t *testing.T) { + turtle, err := convert.Convert("l.sysml", []byte(libraryNamesModel), convert.FormatSysML, convert.FormatTurtle) + if err != nil { + t.Fatal(err) + } + stated := `sysml:qualifiedName "ScalarValues::Real" ;` + if !strings.Contains(string(turtle), stated) { + t.Fatalf("the graph no longer names %q", stated) + } + renamed := strings.Replace(string(turtle), stated, `sysml:qualifiedName "ScalarValues::Rational" ;`, 1) + if back, err := convert.Convert("l.ttl", []byte(renamed), convert.FormatTurtle, convert.FormatSysML); err == nil { + t.Errorf("a misnamed library element was converted:\n%s", back) + } else if !strings.Contains(err.Error(), "library") { + t.Errorf("refused for another reason: %v", err) + } +} diff --git a/tests/export/testdata/convert/action_nodes.golden.ttl b/tests/export/testdata/convert/action_nodes.golden.ttl index e38568576..8e7fb4eb5 100644 --- a/tests/export/testdata/convert/action_nodes.golden.ttl +++ b/tests/export/testdata/convert/action_nodes.golden.ttl @@ -1744,3 +1744,17 @@ expr:ActionNodes__Drive___4017_pexpression_preferent sysml:memberElement elmt:ActionNodes__Drive__brake ; sysml:owner expr:ActionNodes__Drive___4017_pexpression ; sysml:owningRelatedElement expr:ActionNodes__Drive___4017_pexpression . + + + a sysml:ActionUsage ; + sysml:elementId "0cdc3cd3-b06c-5c32-beda-0cf4ba164a64" ; + sysml:qualifiedName "Actions::Action::done" ; + sysml:declaredName "done" ; + sysml:isLibraryElement "true"^^xsd:boolean . + + + a sysml:ActionUsage ; + sysml:elementId "9a0d2905-0f9c-5bb4-af74-9780d6db1817" ; + sysml:qualifiedName "Actions::Action::start" ; + sysml:declaredName "start" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/anonymous_portions.golden.ttl b/tests/export/testdata/convert/anonymous_portions.golden.ttl index a4528b099..63f514b18 100644 --- a/tests/export/testdata/convert/anonymous_portions.golden.ttl +++ b/tests/export/testdata/convert/anonymous_portions.golden.ttl @@ -719,3 +719,24 @@ elmt:AnonymousPortions__car___404_ft0 sysml:owner elmt:AnonymousPortions__car___404 ; sysml:relatedElement elmt:AnonymousPortions__car___404, elmt:AnonymousPortions__Car ; json:relatedElement "[{\"@id\":\"AnonymousPortions__car___404\"},{\"@id\":\"AnonymousPortions__Car\"}]" . + +elmt:dd677401-a352-5fe3-846a-dad08d3eab06 + a sysml:PartUsage ; + sysml:elementId "dd677401-a352-5fe3-846a-dad08d3eab06" ; + sysml:qualifiedName "Parts::Part::done" ; + sysml:declaredName "done" ; + sysml:isLibraryElement "true"^^xsd:boolean . + +elmt:ed80d442-d498-5236-96ae-71a8a52531d2 + a sysml:PartUsage ; + sysml:elementId "ed80d442-d498-5236-96ae-71a8a52531d2" ; + sysml:qualifiedName "Parts::Part::start" ; + sysml:declaredName "start" ; + sysml:isLibraryElement "true"^^xsd:boolean . + +elmt:fa859d04-71c3-510a-bdb7-d35d7bc10bee + a sysml:AttributeUsage ; + sysml:elementId "fa859d04-71c3-510a-bdb7-d35d7bc10bee" ; + sysml:qualifiedName "Occurrences::Occurrence::portionOfLife" ; + sysml:declaredName "portionOfLife" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/condition_references.golden.ttl b/tests/export/testdata/convert/condition_references.golden.ttl index 33eb3152a..482c61657 100644 --- a/tests/export/testdata/convert/condition_references.golden.ttl +++ b/tests/export/testdata/convert/condition_references.golden.ttl @@ -2488,3 +2488,24 @@ expr:ConditionReferences__Rover__inv1___400_pa0_preferent sysml:memberElement elmt:ConditionReferences__Rover__ok ; sysml:owner expr:ConditionReferences__Rover__inv1___400_pa0 ; sysml:owningRelatedElement expr:ConditionReferences__Rover__inv1___400_pa0 . + + + a sysml:DataType ; + sysml:elementId "14c0aa22-5489-59b5-b438-ded26e83ba31" ; + sysml:qualifiedName "ScalarValues::Real" ; + sysml:declaredName "Real" ; + sysml:isLibraryElement "true"^^xsd:boolean . + + + a sysml:LibraryPackage ; + sysml:elementId "40bb440c-5036-58e1-8675-5afccb8b8f1d" ; + sysml:qualifiedName "ScalarValues" ; + sysml:declaredName "ScalarValues" ; + sysml:isLibraryElement "true"^^xsd:boolean . + +elmt:d1e9242d-b2e3-5270-bf69-4f4fb0447193 + a sysml:DataType ; + sysml:elementId "d1e9242d-b2e3-5270-bf69-4f4fb0447193" ; + sysml:qualifiedName "ScalarValues::Boolean" ; + sysml:declaredName "Boolean" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/imports.golden.ttl b/tests/export/testdata/convert/imports.golden.ttl index 9934c76ba..e37678721 100644 --- a/tests/export/testdata/convert/imports.golden.ttl +++ b/tests/export/testdata/convert/imports.golden.ttl @@ -138,3 +138,23 @@ elmt:Importer__Deep__Item_om sysml:ownedRelatedElement elmt:Importer__Deep__Item ; sysml:owningRelatedElement elmt:Importer__Deep ; sysml:membershipOwningNamespace elmt:Importer__Deep . + + + a sysml:DataType ; + sysml:elementId "14c0aa22-5489-59b5-b438-ded26e83ba31" ; + sysml:qualifiedName "ScalarValues::Real" ; + sysml:declaredName "Real" ; + sysml:isLibraryElement "true"^^xsd:boolean . + +elmt:ab72a695-5fe9-58a3-9d48-9e9a8711862d + a sysml:OwningMembership ; + sysml:elementId "ab72a695-5fe9-58a3-9d48-9e9a8711862d" ; + sysml:memberElement ; + sysml:isLibraryElement "true"^^xsd:boolean . + + + a sysml:LibraryPackage ; + sysml:elementId "3fcc2ef4-a31c-522b-b69d-717d74bccfa6" ; + sysml:qualifiedName "ISQ" ; + sysml:declaredName "ISQ" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/invariant_references.golden.ttl b/tests/export/testdata/convert/invariant_references.golden.ttl index ab911b8d3..c72a36b7f 100644 --- a/tests/export/testdata/convert/invariant_references.golden.ttl +++ b/tests/export/testdata/convert/invariant_references.golden.ttl @@ -1035,3 +1035,24 @@ elmt:InvariantReferences__Tank__bounded___401_referent sysml:memberElement elmt:InvariantReferences__Tank__ready ; sysml:owner elmt:InvariantReferences__Tank__bounded___401 ; sysml:owningRelatedElement elmt:InvariantReferences__Tank__bounded___401 . + + + a sysml:DataType ; + sysml:elementId "14c0aa22-5489-59b5-b438-ded26e83ba31" ; + sysml:qualifiedName "ScalarValues::Real" ; + sysml:declaredName "Real" ; + sysml:isLibraryElement "true"^^xsd:boolean . + + + a sysml:LibraryPackage ; + sysml:elementId "40bb440c-5036-58e1-8675-5afccb8b8f1d" ; + sysml:qualifiedName "ScalarValues" ; + sysml:declaredName "ScalarValues" ; + sysml:isLibraryElement "true"^^xsd:boolean . + +elmt:d1e9242d-b2e3-5270-bf69-4f4fb0447193 + a sysml:DataType ; + sysml:elementId "d1e9242d-b2e3-5270-bf69-4f4fb0447193" ; + sysml:qualifiedName "ScalarValues::Boolean" ; + sysml:declaredName "Boolean" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/irregular_layout.golden.ttl b/tests/export/testdata/convert/irregular_layout.golden.ttl index 2fe9571f3..ffab4cd69 100644 --- a/tests/export/testdata/convert/irregular_layout.golden.ttl +++ b/tests/export/testdata/convert/irregular_layout.golden.ttl @@ -3500,3 +3500,17 @@ elmt:IrregularLayout__Drive___404___400_rs sysml:owner elmt:IrregularLayout__Drive___404___400 ; sysml:relatedElement elmt:IrregularLayout__Drive___404___400, elmt:IrregularLayout__Drive__tick ; json:relatedElement "[{\"@id\":\"IrregularLayout__Drive___404___400\"},{\"@id\":\"IrregularLayout__Drive__tick\"}]" . + + + a sysml:DataType ; + sysml:elementId "14c0aa22-5489-59b5-b438-ded26e83ba31" ; + sysml:qualifiedName "ScalarValues::Real" ; + sysml:declaredName "Real" ; + sysml:isLibraryElement "true"^^xsd:boolean . + + + a sysml:LibraryPackage ; + sysml:elementId "40bb440c-5036-58e1-8675-5afccb8b8f1d" ; + sysml:qualifiedName "ScalarValues" ; + sysml:declaredName "ScalarValues" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/loop_scopes.golden.ttl b/tests/export/testdata/convert/loop_scopes.golden.ttl index f18b437e4..df482380a 100644 --- a/tests/export/testdata/convert/loop_scopes.golden.ttl +++ b/tests/export/testdata/convert/loop_scopes.golden.ttl @@ -631,3 +631,10 @@ expr:Loops__Charge___402_puntilCondition_pa0_preferent sysml:memberElement elmt:Loops__Charge___402__pace ; sysml:owner expr:Loops__Charge___402_puntilCondition_pa0 ; sysml:owningRelatedElement expr:Loops__Charge___402_puntilCondition_pa0 . + + + a sysml:ActionUsage ; + sysml:elementId "0cdc3cd3-b06c-5c32-beda-0cf4ba164a64" ; + sysml:qualifiedName "Actions::Action::done" ; + sysml:declaredName "done" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/parts.golden.ttl b/tests/export/testdata/convert/parts.golden.ttl index f41058e51..fe47b9de4 100644 --- a/tests/export/testdata/convert/parts.golden.ttl +++ b/tests/export/testdata/convert/parts.golden.ttl @@ -230,3 +230,10 @@ elmt:Vehicles__Vehicle__engine_ft0 sysml:owner elmt:Vehicles__Vehicle__engine ; sysml:relatedElement elmt:Vehicles__Vehicle__engine, elmt:Vehicles__Engine ; json:relatedElement "[{\"@id\":\"Vehicles__Vehicle__engine\"},{\"@id\":\"Vehicles__Engine\"}]" . + + + a sysml:AttributeDefinition ; + sysml:elementId "9cd0e404-efee-50e5-a59b-681065bd188c" ; + sysml:qualifiedName "ISQBase::MassValue" ; + sysml:declaredName "MassValue" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/quoted_succession_ends.golden.ttl b/tests/export/testdata/convert/quoted_succession_ends.golden.ttl index 0e56cb300..f9d7b5e03 100644 --- a/tests/export/testdata/convert/quoted_succession_ends.golden.ttl +++ b/tests/export/testdata/convert/quoted_succession_ends.golden.ttl @@ -724,3 +724,17 @@ elmt:QuotedSuccessionEnds__drive__drive_20vehicle_om sysml:membershipOwningNamespace elmt:QuotedSuccessionEnds__drive ; sysml:ownedMemberFeature elmt:QuotedSuccessionEnds__drive__drive_20vehicle ; sysml:owningType elmt:QuotedSuccessionEnds__drive . + + + a sysml:ActionUsage ; + sysml:elementId "0cdc3cd3-b06c-5c32-beda-0cf4ba164a64" ; + sysml:qualifiedName "Actions::Action::done" ; + sysml:declaredName "done" ; + sysml:isLibraryElement "true"^^xsd:boolean . + + + a sysml:ActionUsage ; + sysml:elementId "9a0d2905-0f9c-5bb4-af74-9780d6db1817" ; + sysml:qualifiedName "Actions::Action::start" ; + sysml:declaredName "start" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/relationship_clause_order.golden.ttl b/tests/export/testdata/convert/relationship_clause_order.golden.ttl index 235064518..38db445a7 100644 --- a/tests/export/testdata/convert/relationship_clause_order.golden.ttl +++ b/tests/export/testdata/convert/relationship_clause_order.golden.ttl @@ -425,3 +425,17 @@ elmt:ClauseOrder__Mission__groundSystemAtIngress_ss0 sysml:owner elmt:ClauseOrder__Mission__groundSystemAtIngress ; sysml:relatedElement elmt:ClauseOrder__Mission__groundSystemAtIngress, elmt:ClauseOrder__Mission__context ; json:relatedElement "[{\"@id\":\"ClauseOrder__Mission__groundSystemAtIngress\"},{\"@id\":\"ClauseOrder__Mission__context\"}]" . + + + a sysml:DataType ; + sysml:elementId "14c0aa22-5489-59b5-b438-ded26e83ba31" ; + sysml:qualifiedName "ScalarValues::Real" ; + sysml:declaredName "Real" ; + sysml:isLibraryElement "true"^^xsd:boolean . + + + a sysml:LibraryPackage ; + sysml:elementId "40bb440c-5036-58e1-8675-5afccb8b8f1d" ; + sysml:qualifiedName "ScalarValues" ; + sysml:declaredName "ScalarValues" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/shadowing_body.golden.ttl b/tests/export/testdata/convert/shadowing_body.golden.ttl index 9426020db..f148b331a 100644 --- a/tests/export/testdata/convert/shadowing_body.golden.ttl +++ b/tests/export/testdata/convert/shadowing_body.golden.ttl @@ -717,3 +717,17 @@ expr:ShadowBody__Sensor__highs_pvalue_pa0_presult_pa0_preferent sysml:memberElement "value" ; sysml:owner expr:ShadowBody__Sensor__highs_pvalue_pa0_presult_pa0 ; sysml:owningRelatedElement expr:ShadowBody__Sensor__highs_pvalue_pa0_presult_pa0 . + + + a sysml:LibraryPackage ; + sysml:elementId "1ed3f10f-1391-5a27-a5bd-233a70b01328" ; + sysml:qualifiedName "ControlFunctions" ; + sysml:declaredName "ControlFunctions" ; + sysml:isLibraryElement "true"^^xsd:boolean . + +elmt:d2b84130-4d51-5e96-9ce6-150b5bd9ff56 + a sysml:Function ; + sysml:elementId "d2b84130-4d51-5e96-9ce6-150b5bd9ff56" ; + sysml:qualifiedName "ControlFunctions::select" ; + sysml:declaredName "select" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/then_after_members.golden.ttl b/tests/export/testdata/convert/then_after_members.golden.ttl index fc4d6555b..3a174d323 100644 --- a/tests/export/testdata/convert/then_after_members.golden.ttl +++ b/tests/export/testdata/convert/then_after_members.golden.ttl @@ -9683,3 +9683,31 @@ elmt:ThenAfterMembers__Switch__pressed_ft0 sysml:owner elmt:ThenAfterMembers__Switch__pressed ; sysml:relatedElement elmt:ThenAfterMembers__Switch__pressed, elmt:d1e9242d-b2e3-5270-bf69-4f4fb0447193 ; json:relatedElement "[{\"@id\":\"ThenAfterMembers__Switch__pressed\"},{\"@id\":\"d1e9242d-b2e3-5270-bf69-4f4fb0447193\"}]" . + + + a sysml:ActionUsage ; + sysml:elementId "0cdc3cd3-b06c-5c32-beda-0cf4ba164a64" ; + sysml:qualifiedName "Actions::Action::done" ; + sysml:declaredName "done" ; + sysml:isLibraryElement "true"^^xsd:boolean . + + + a sysml:ActionUsage ; + sysml:elementId "9a0d2905-0f9c-5bb4-af74-9780d6db1817" ; + sysml:qualifiedName "Actions::Action::start" ; + sysml:declaredName "start" ; + sysml:isLibraryElement "true"^^xsd:boolean . + +elmt:d1e9242d-b2e3-5270-bf69-4f4fb0447193 + a sysml:DataType ; + sysml:elementId "d1e9242d-b2e3-5270-bf69-4f4fb0447193" ; + sysml:qualifiedName "ScalarValues::Boolean" ; + sysml:declaredName "Boolean" ; + sysml:isLibraryElement "true"^^xsd:boolean . + +elmt:f2350199-2ab1-5258-8514-58812ef25dc6 + a sysml:DataType ; + sysml:elementId "f2350199-2ab1-5258-8514-58812ef25dc6" ; + sysml:qualifiedName "ScalarValues::Integer" ; + sysml:declaredName "Integer" ; + sysml:isLibraryElement "true"^^xsd:boolean . diff --git a/tests/export/testdata/convert/views_flows_parameters.golden.ttl b/tests/export/testdata/convert/views_flows_parameters.golden.ttl index 985487639..56b25ca02 100644 --- a/tests/export/testdata/convert/views_flows_parameters.golden.ttl +++ b/tests/export/testdata/convert/views_flows_parameters.golden.ttl @@ -1389,3 +1389,10 @@ elmt:FlowsAndViews__vehicleView_ft0 sysml:owner elmt:FlowsAndViews__vehicleView ; sysml:relatedElement elmt:FlowsAndViews__vehicleView, elmt:FlowsAndViews__StructureView ; json:relatedElement "[{\"@id\":\"FlowsAndViews__vehicleView\"},{\"@id\":\"FlowsAndViews__StructureView\"}]" . + + + a sysml:MetadataDefinition ; + sysml:elementId "550f9923-23f8-5142-aa06-d7ff476876d7" ; + sysml:qualifiedName "SysML::Systems::PartUsage" ; + sysml:declaredName "PartUsage" ; + sysml:isLibraryElement "true"^^xsd:boolean . From 77cc41f86af2757e51203f3745ca496429ff657c Mon Sep 17 00:00:00 2001 From: Somesh Kashyap Date: Wed, 30 Sep 2026 08:20:35 -0400 Subject: [PATCH 2/2] fix(export): check a library membership against the library; read either true Review: - a library membership is refused unless the library gives its id to the owning membership of the element its memberElement names, so an import of it cannot silently name another element; - isLibraryElement is read as an xsd:boolean, so "1" marks a library element as "true" does, in the reader, the root namespace and sync. Co-Authored-By: Claude Opus 5.5 --- internal/translate/export/rdf_in.go | 21 +++++++-- internal/translate/export/root_namespace.go | 3 +- tests/export/library_names_test.go | 47 +++++++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/internal/translate/export/rdf_in.go b/internal/translate/export/rdf_in.go index 2203886b8..5c8b4bee9 100644 --- a/internal/translate/export/rdf_in.go +++ b/internal/translate/export/rdf_in.go @@ -1258,11 +1258,26 @@ func (d *decoder) libraryStubs() (map[string]bool, error) { if stated, ok := d.graph.Lexical(subject, rdf.SysML+pElementID); ok { id = stated } - if member, ok := d.graph.Object(subject, rdf.SysML+pMemberElement); ok { - if lexical, ok := d.graph.Lexical(member, rdf.SysML+pIsLibraryElement); !ok || lexical != "true" { + if members := d.graph.Objects(subject, rdf.SysML+pMemberElement); len(members) > 0 { + // A library membership is the owning membership the library gives + // its id, and owns the element the library says it owns. + catalog := identity.LibraryCatalog(libs.NewModelIndex()) + owned, ok := catalog.OwningMembership(id) + if !ok { + return nil, &UnsupportedError{ + What: fmt.Sprintf("the library membership <%s>", subject.Value), + Note: fmt.Sprintf("it is marked sysml:isLibraryElement, but the bundled standard library has no owning membership with id %q", id), + } + } + member := members[0] + memberID := rdf.LocalName(member.Value) + if stated, ok := d.graph.Lexical(member, rdf.SysML+pElementID); ok { + memberID = stated + } + if len(members) != 1 || !member.IsIRI() || !d.graph.BoolValue(member, rdf.SysML+pIsLibraryElement) || memberID != owned.ID { return nil, &UnsupportedError{ What: fmt.Sprintf("the library membership <%s>", subject.Value), - Note: "its memberElement is no library element, and a library membership owns one", + Note: fmt.Sprintf("the bundled standard library gives id %q to the owning membership of %s (id %q), and its memberElement is not that library element", id, owned.FQN, owned.ID), } } stubs[subject.Value] = true diff --git a/internal/translate/export/root_namespace.go b/internal/translate/export/root_namespace.go index 93fae799e..dab4ce05d 100644 --- a/internal/translate/export/root_namespace.go +++ b/internal/translate/export/root_namespace.go @@ -270,8 +270,7 @@ func unownedElements(graph *rdf.Graph) []rdf.Term { // (or its owning membership) the graph references rather than declares: marked // sysml:isLibraryElement, with no owner in the graph. func LibraryReference(graph *rdf.Graph, subject rdf.Term) bool { - lexical, ok := graph.Lexical(subject, rdf.SysML+pIsLibraryElement) - return ok && lexical == "true" && !hasOwner(graph, subject) && + return graph.BoolValue(subject, rdf.SysML+pIsLibraryElement) && !hasOwner(graph, subject) && !graph.HasProperty(subject, rdf.SysML+pMembershipOwningNamespace) } diff --git a/tests/export/library_names_test.go b/tests/export/library_names_test.go index 9cfa7c328..5fa054930 100644 --- a/tests/export/library_names_test.go +++ b/tests/export/library_names_test.go @@ -138,3 +138,50 @@ func TestLibraryNameThatDisagreesIsRefused(t *testing.T) { t.Errorf("refused for another reason: %v", err) } } + +// A library membership owns the element the library says its id owns: one +// whose memberElement is another library element is refused, not read as an +// import of either. +func TestLibraryMembershipOwningAnotherElementIsRefused(t *testing.T) { + turtle, err := convert.Convert("l.sysml", []byte(libraryNamesModel), convert.FormatSysML, convert.FormatTurtle) + if err != nil { + t.Fatal(err) + } + real := "sysml:memberElement ;\n sysml:isLibraryElement" + if !strings.Contains(string(turtle), real) { + t.Fatalf("the graph no longer states the library membership of Real") + } + integer := strings.Index(string(turtle), `sysml:qualifiedName "ScalarValues::Integer"`) + if integer < 0 { + t.Fatal("the graph no longer names ScalarValues::Integer") + } + start := strings.LastIndex(string(turtle[:integer]), "\n\n") + 2 + subject := strings.SplitN(string(turtle[start:]), "\n", 2)[0] + swapped := strings.Replace(string(turtle), real, "sysml:memberElement "+subject+" ;\n sysml:isLibraryElement", 1) + if back, err := convert.Convert("l.ttl", []byte(swapped), convert.FormatTurtle, convert.FormatSysML); err == nil { + t.Errorf("a library membership owning another element was converted:\n%s", back) + } else if !strings.Contains(err.Error(), "library membership") { + t.Errorf("refused for another reason: %v", err) + } +} + +// isLibraryElement is an xsd:boolean, so "1" marks a library element as +// "true" does: the element is still a reference, not a declaration. +func TestLibraryMarkerReadsEitherBooleanSpelling(t *testing.T) { + turtle, err := convert.Convert("l.sysml", []byte(libraryNamesModel), convert.FormatSysML, convert.FormatTurtle) + if err != nil { + t.Fatal(err) + } + marked := `sysml:isLibraryElement "true"^^xsd:boolean` + if !strings.Contains(string(turtle), marked) { + t.Fatalf("the graph no longer marks library elements") + } + ones := strings.ReplaceAll(string(turtle), marked, `sysml:isLibraryElement "1"^^xsd:boolean`) + back, err := convert.Convert("l.ttl", []byte(ones), convert.FormatTurtle, convert.FormatSysML) + if err != nil { + t.Fatalf("a graph marking library elements with 1 was refused: %v", err) + } + if strings.Contains(string(back), "datatype") || strings.Contains(string(back), "library package") { + t.Errorf("a library element marked with 1 was written back as a declaration:\n%s", back) + } +}