diff --git a/changes/unreleased/extension-library-element-ids.fixed.md b/changes/unreleased/extension-library-element-ids.fixed.md new file mode 100644 index 0000000000..5dc65a59c9 --- /dev/null +++ b/changes/unreleased/extension-library-element-ids.fixed.md @@ -0,0 +1 @@ +- A reference into one of OpenSysML's own libraries (`DocumentQueries`, `MOSA`, `OOSEM`, …) — a redefined or typing feature, a metadata or view usage's type, a feature reference's referent, an imported namespace — is written by element id (`{"@id": …}` in the API element form, an IRI with a library stub in Turtle) instead of a `{"@ref": "name"}` name reference, since every element has an `elementId` (KerML 8.3.2.1.2). The extension libraries' elements carry fixed, deterministic ids derived as the standard library's normative ids are, under the libraries' upstream URL rather than an OMG specification URL; hover reports one as `(extension, OpenSysML)`. diff --git a/docs/project/api-json-differential-baseline.json b/docs/project/api-json-differential-baseline.json index c387572a29..08030d9e2d 100644 --- a/docs/project/api-json-differential-baseline.json +++ b/docs/project/api-json-differential-baseline.json @@ -14,7 +14,7 @@ { "name": "api-json-writer", "source": "internal/translate/export", - "sourceDigest": "sha256:0f409cf056b724f12c7a19ba55649418cf10a845df23fbea4e3183eb0f604b3a" + "sourceDigest": "sha256:54757c5957897eb6f02f64d8eccb8136cd49bc54a7e42300532f1d37d03a37cb" } ], "inputs": [ @@ -113,23 +113,23 @@ "pilotFailed": 56, "pilotLibraryElements": 30926, "elements": { - "matched": 89957, + "matched": 90023, "oursOnly": 28511, - "pilotOnly": 38863, + "pilotOnly": 38797, "pilotOnlyImplied": 0, "typeMismatch": 1742 }, "properties": { - "presentEqual": 618674, - "presentDifferent": 50027, - "oursMissing": 1058325, - "pilotMissing": 68494, - "absentBoth": 2102500, - "indeterminate": 21034 + "presentEqual": 619268, + "presentDifferent": 45688, + "oursMissing": 1058259, + "pilotMissing": 68560, + "absentBoth": 2103094, + "indeterminate": 25703 }, "rows": 11311, - "unkeyedReferences": 103180, - "extensionProperties": 146792, + "unkeyedReferences": 108366, + "extensionProperties": 146858, "duplicateElements": { "ours": 560, "pilot": 78022 @@ -302,11 +302,11 @@ }, "properties": { "presentEqual": 44282, - "presentDifferent": 6034, + "presentDifferent": 6029, "oursMissing": 71586, "pilotMissing": 5182, "absentBoth": 142032, - "indeterminate": 1853 + "indeterminate": 1858 }, "duplicateElements": { "ours": 0, @@ -338,11 +338,11 @@ }, "properties": { "presentEqual": 7820, - "presentDifferent": 393, + "presentDifferent": 383, "oursMissing": 13187, "pilotMissing": 877, "absentBoth": 28133, - "indeterminate": 316 + "indeterminate": 326 }, "duplicateElements": { "ours": 25, @@ -366,19 +366,19 @@ "oursColliding": 3, "pilotLibraryElements": 13888, "elements": { - "matched": 26005, + "matched": 26071, "oursOnly": 8734, - "pilotOnly": 15010, + "pilotOnly": 14944, "pilotOnlyImplied": 0, "typeMismatch": 200 }, "properties": { - "presentEqual": 181279, - "presentDifferent": 13823, - "oursMissing": 316392, - "pilotMissing": 20267, - "absentBoth": 636241, - "indeterminate": 6231 + "presentEqual": 181873, + "presentDifferent": 10948, + "oursMissing": 316326, + "pilotMissing": 20333, + "absentBoth": 636835, + "indeterminate": 9436 }, "duplicateElements": { "ours": 428, @@ -444,11 +444,11 @@ }, "properties": { "presentEqual": 47574, - "presentDifferent": 4366, + "presentDifferent": 2917, "oursMissing": 88453, "pilotMissing": 4949, "absentBoth": 163532, - "indeterminate": 626 + "indeterminate": 2075 }, "duplicateElements": { "ours": 0, @@ -14787,11 +14787,11 @@ ], "CalculationUsage.type": [ 72, - 7, + 3, 35, 0, 0, - 0 + 4 ], "CalculationUsage.unioningType": [ 0, @@ -33891,11 +33891,11 @@ ], "FeatureReferenceExpression.referent": [ 1613, - 99, + 71, 0, 0, 0, - 436 + 464 ], "FeatureReferenceExpression.result": [ 2148, @@ -33955,11 +33955,11 @@ ], "FeatureTyping.general": [ 3336, - 668, + 336, 4, 0, 0, - 1904 + 2236 ], "FeatureTyping.isImplied": [ 0, @@ -34107,11 +34107,11 @@ ], "FeatureTyping.relatedElement": [ 3336, - 668, + 336, 4, 0, 0, - 1904 + 2236 ], "FeatureTyping.source": [ 5908, @@ -34131,11 +34131,11 @@ ], "FeatureTyping.target": [ 3336, - 668, + 336, 4, 0, 0, - 1904 + 2236 ], "FeatureTyping.textualRepresentation": [ 0, @@ -34147,11 +34147,11 @@ ], "FeatureTyping.type": [ 3338, - 670, + 338, 0, 0, 0, - 1904 + 2236 ], "FeatureTyping.typedFeature": [ 5910, @@ -44243,11 +44243,11 @@ ], "InvocationExpression.function": [ 112, - 245, + 19, 4, 0, 0, - 107 + 333 ], "InvocationExpression.importedMembership": [ 0, @@ -46203,11 +46203,11 @@ ], "ItemUsage.type": [ 243, - 20, + 16, 87, 0, 0, - 16 + 20 ], "ItemUsage.unioningType": [ 0, @@ -49691,11 +49691,11 @@ ], "Membership.memberElement": [ 2145, - 353, + 99, 0, 0, 0, - 646 + 900 ], "Membership.memberElementId": [ 0, @@ -50035,11 +50035,11 @@ ], "MembershipImport.importedMembership": [ 107, - 15, + 13, 0, 0, 0, - 265 + 267 ], "MembershipImport.isImplied": [ 0, @@ -53883,11 +53883,11 @@ ], "MetadataUsage.type": [ 150, - 147, + 13, 0, 0, 0, - 82 + 216 ], "MetadataUsage.unioningType": [ 0, @@ -55331,11 +55331,11 @@ ], "NamespaceImport.importedNamespace": [ 310, - 25, + 8, 0, 0, 0, - 331 + 348 ], "NamespaceImport.isImplied": [ 0, @@ -60515,11 +60515,11 @@ ], "PartUsage.type": [ 1011, - 235, + 63, 706, 0, 0, - 0 + 172 ], "PartUsage.unioningType": [ 0, @@ -64110,7 +64110,7 @@ 0, 0, 0, - 2506, + 2572, 0 ], "Redefinition.declaredName": [ @@ -64126,23 +64126,23 @@ 0, 0, 0, - 2506, + 2572, 0 ], "Redefinition.general": [ 1575, - 387, + 57, 2, 0, 0, - 542 + 938 ], "Redefinition.isImplied": [ 0, 0, 0, 0, - 2506, + 2572, 0 ], "Redefinition.isImpliedIncluded": [ @@ -64150,13 +64150,13 @@ 0, 0, 0, - 2506, + 2572, 0 ], "Redefinition.isLibraryElement": [ 0, 0, - 2506, + 2572, 0, 0, 0 @@ -64174,7 +64174,7 @@ 0, 0, 0, - 2506, + 2572, 0 ], "Redefinition.ownedElement": [ @@ -64182,7 +64182,7 @@ 0, 0, 0, - 2506, + 2572, 0 ], "Redefinition.ownedRelatedElement": [ @@ -64190,7 +64190,7 @@ 0, 0, 0, - 2498, + 2564, 0 ], "Redefinition.ownedRelationship": [ @@ -64198,19 +64198,19 @@ 0, 0, 0, - 2506, + 2572, 0 ], "Redefinition.owner": [ 2, 0, 0, - 2504, + 2570, 0, 0 ], "Redefinition.owningFeature": [ - 2504, + 2570, 0, 0, 0, @@ -64234,7 +64234,7 @@ 0 ], "Redefinition.owningRelatedElement": [ - 2504, + 2570, 0, 0, 0, @@ -64252,7 +64252,7 @@ "Redefinition.owningType": [ 0, 0, - 2504, + 2570, 0, 0, 0 @@ -64267,14 +64267,14 @@ ], "Redefinition.redefinedFeature": [ 1576, - 388, + 58, 0, 0, 0, - 542 + 938 ], "Redefinition.redefiningFeature": [ - 2505, + 2571, 1, 0, 0, @@ -64283,14 +64283,14 @@ ], "Redefinition.relatedElement": [ 1575, - 387, + 57, 2, 0, 0, - 542 + 938 ], "Redefinition.source": [ - 2504, + 2570, 0, 2, 0, @@ -64298,7 +64298,7 @@ 0 ], "Redefinition.specific": [ - 2504, + 2570, 0, 2, 0, @@ -64307,14 +64307,14 @@ ], "Redefinition.subsettedFeature": [ 1575, - 387, + 57, 2, 0, 0, - 542 + 938 ], "Redefinition.subsettingFeature": [ - 2504, + 2570, 0, 2, 0, @@ -64323,18 +64323,18 @@ ], "Redefinition.target": [ 1575, - 387, + 57, 2, 0, 0, - 542 + 938 ], "Redefinition.textualRepresentation": [ 0, 0, 0, 0, - 2506, + 2572, 0 ], "ReferenceSubsetting.aliasIds": [ @@ -64355,11 +64355,11 @@ ], "ReferenceSubsetting.general": [ 316, - 40, + 38, 0, 0, 0, - 24 + 26 ], "ReferenceSubsetting.isImplied": [ 0, @@ -64451,11 +64451,11 @@ ], "ReferenceSubsetting.referencedFeature": [ 316, - 40, + 38, 0, 0, 0, - 24 + 26 ], "ReferenceSubsetting.referencingFeature": [ 380, @@ -64467,11 +64467,11 @@ ], "ReferenceSubsetting.relatedElement": [ 316, - 40, + 38, 0, 0, 0, - 24 + 26 ], "ReferenceSubsetting.source": [ 380, @@ -64491,11 +64491,11 @@ ], "ReferenceSubsetting.subsettedFeature": [ 316, - 40, + 38, 0, 0, 0, - 24 + 26 ], "ReferenceSubsetting.subsettingFeature": [ 380, @@ -64507,11 +64507,11 @@ ], "ReferenceSubsetting.target": [ 316, - 40, + 38, 0, 0, 0, - 24 + 26 ], "ReferenceSubsetting.textualRepresentation": [ 0, @@ -64557,7 +64557,7 @@ 1422, 0, 0, - 70, + 4, 0, 0 ], @@ -65234,9 +65234,9 @@ 0 ], "ReferenceUsage.ownedRedefinition": [ - 642, + 708, 0, - 70, + 4, 0, 1482, 0 @@ -65258,17 +65258,17 @@ 0 ], "ReferenceUsage.ownedSpecialization": [ - 1584, + 1650, 75, - 70, + 4, 0, 465, 0 ], "ReferenceUsage.ownedSubsetting": [ - 742, + 808, 22, - 72, + 6, 0, 1358, 0 @@ -65373,7 +65373,7 @@ 0, 0, 0, - 642, + 708, 0, 0 ], @@ -65403,11 +65403,11 @@ ], "ReferenceUsage.type": [ 413, - 48, + 46, 1232, 0, 0, - 501 + 503 ], "ReferenceUsage.unioningType": [ 0, @@ -75251,11 +75251,11 @@ ], "Subclassification.general": [ 349, - 127, + 14, 0, 0, 0, - 136 + 249 ], "Subclassification.isImplied": [ 0, @@ -75387,11 +75387,11 @@ ], "Subclassification.relatedElement": [ 347, - 127, + 14, 2, 0, 0, - 136 + 249 ], "Subclassification.source": [ 610, @@ -75419,19 +75419,19 @@ ], "Subclassification.superclassifier": [ 348, - 126, + 13, 2, 0, 0, - 136 + 249 ], "Subclassification.target": [ 348, - 126, + 13, 2, 0, 0, - 136 + 249 ], "Subclassification.textualRepresentation": [ 0, @@ -75683,11 +75683,11 @@ ], "Subsetting.general": [ 309, - 43, + 33, 2, 0, 0, - 425 + 435 ], "Subsetting.isImplied": [ 0, @@ -75819,11 +75819,11 @@ ], "Subsetting.relatedElement": [ 308, - 44, + 34, 2, 0, 0, - 425 + 435 ], "Subsetting.source": [ 777, @@ -75843,11 +75843,11 @@ ], "Subsetting.subsettedFeature": [ 310, - 44, + 34, 0, 0, 0, - 425 + 435 ], "Subsetting.subsettingFeature": [ 778, @@ -75859,11 +75859,11 @@ ], "Subsetting.target": [ 309, - 43, + 33, 2, 0, 0, - 425 + 435 ], "Subsetting.textualRepresentation": [ 0, @@ -88339,11 +88339,11 @@ ], "ViewUsage.type": [ 9, - 19, + 3, 41, 0, 0, - 29 + 45 ], "ViewUsage.unioningType": [ 0, diff --git a/docs/project/api-json-differential.md b/docs/project/api-json-differential.md index 8b8284ac7a..5c354104e0 100644 --- a/docs/project/api-json-differential.md +++ b/docs/project/api-json-differential.md @@ -190,9 +190,9 @@ The pinned pilot's verdict on the probe (`validate-sysml-batch`): clean. ## Results (pilot `2026-08`, 920 files) -**Scorecard:** 920 files; 89957 elements matched (28511 ours-only, 38863 pilot-only, 0 of them implied); properties: 618674 present-equal, 50027 present-different, 1058325 ours-missing, 68494 pilot-missing, 2102500 absent-both, 21034 indeterminate. +**Scorecard:** 920 files; 90023 elements matched (28511 ours-only, 38797 pilot-only, 0 of them implied); properties: 619268 present-equal, 45688 present-different, 1058259 ours-missing, 68560 pilot-missing, 2103094 absent-both, 25703 indeterminate. -The pilot's export failed on 56 files (`pilotFailed`: its own export throws on an element without a resource, 52 of them specification examples and 4 test fixtures; the files are named in the run's text report), ours refused 36 (all of them specification examples the parser could not read whole), left the pilot's 56 failed files out and converted 24 documents alone for declaring an element another document of their root declares (`oursColliding`); no root fell back to per-document conversion. 11311 (metaclass, property) rows carry a comparison; 103180 references compared through the unkeyed sentinel, 146792 of our `sysx:` annotations were left out, and 289/345 elements (ours/pilot) were dropped for ambiguous keys, 8198/31310 for having none. +The pilot's export failed on 56 files (`pilotFailed`: its own export throws on an element without a resource, 52 of them specification examples and 4 test fixtures; the files are named in the run's text report), ours refused 36 (all of them specification examples the parser could not read whole), left the pilot's 56 failed files out and converted 24 documents alone for declaring an element another document of their root declares (`oursColliding`); no root fell back to per-document conversion. 11311 (metaclass, property) rows carry a comparison; 108366 references compared through the unkeyed sentinel, 146858 of our `sysx:` annotations were left out, and 289/345 elements (ours/pilot) were dropped for ambiguous keys, 8198/31310 for having none. | root | files | matched | ours-only | pilot-only | present-equal | present-different | ours-missing | pilot-missing | |---|---:|---:|---:|---:|---:|---:|---:|---:| @@ -200,11 +200,11 @@ The pilot's export failed on 56 files (`pilotFailed`: its own export throws on a | `examples/pilot-corpora/sysml-examples` | 99 | 25778 | 10038 | 10488 | 180567 | 11831 | 308696 | 19510 | | `examples/pilot-corpora/sysml-validation` | 56 | 9246 | 2967 | 3341 | 63000 | 6100 | 108814 | 7092 | | `examples/pilot-corpora/kerml-examples` | 58 | 6387 | 1637 | 1854 | 43671 | 3292 | 63793 | 5268 | -| `examples/spec-examples` | 511 | 6980 | 1966 | 2728 | 44282 | 6034 | 71586 | 5182 | -| `tests/testdata` | 32 | 1201 | 91 | 487 | 7820 | 393 | 13187 | 877 | -| `examples` | 52 | 26005 | 8734 | 15010 | 181279 | 13823 | 316392 | 20267 | +| `examples/spec-examples` | 511 | 6980 | 1966 | 2728 | 44282 | 6029 | 71586 | 5182 | +| `tests/testdata` | 32 | 1201 | 91 | 487 | 7820 | 383 | 13187 | 877 | +| `examples` | 52 | 26071 | 8734 | 14944 | 181873 | 10948 | 316326 | 20333 | | `tools/referee/diff/testdata` | 4 | 53 | 0 | 0 | 346 | 17 | 462 | 43 | -| `docs/manual/examples` | 8 | 7020 | 947 | 2082 | 47574 | 4366 | 88453 | 4949 | +| `docs/manual/examples` | 8 | 7020 | 947 | 2082 | 47574 | 2917 | 88453 | 4949 | The scorecard is mostly **ours-missing**: this repository's writer emits a subset of the metamodel's properties, and the referee now measures which, per metaclass. The full ranked scorecard with one concrete example per differing row is `build/pilot-apijson-diff/pilot-apijson-diff.txt` after a run; the baseline keeps every row's five counts. @@ -265,22 +265,23 @@ in six files the pilot already exported, and the 1712 elements of `mosa-demo/mos without a resource) while `MOSA` and `OOSEM` were unloaded. `tests/testdata` matched the same 1201 elements with two more properties present-equal; no other root moved, and the pilot reported no diagnostic on any library file (its batch validator reads the sixteen as one resource set without -an error). The members that gained a match disagree with the pilot in three shapes, none of them a -derivation this repository gets wrong and none new — each is a shape the scorecard already carried -for the standard library or for members the model declares itself, now measured on library-backed -members too: +an error). The members that gained a match disagreed with the pilot in three shapes when the libraries were +first loaded, none of them a derivation this repository gets wrong and none new — each a shape the +scorecard already carried for the standard library or for members the model declares itself, now +measured on library-backed members too. The first is closed (next section); the other two remain: -- a reference to an element of this repository's libraries is spelled as a name, `{"@ref": +- a reference to an element of this repository's libraries was spelled as a name, `{"@ref": "caption"}`, where the pilot writes the element: `Redefinition::redefinedFeature` (KerML 8.3.3.3.8) on all 327 of those redefinitions, `FeatureTyping::type` (8.3.3.3.7) on 157, the `type` of 123 `MetadataUsage` and 16 `ViewUsage` typed by `MOSA` and `OOSEM` metadata and view definitions, `FeatureReferenceExpression::referent` (8.3.4.8.5) and the `memberElement` of its `Membership` on 28 enumeration literals, and `ReferenceSubsetting::referencedFeature` on the two - `render` members. The writer gives a library element an id only where the pilot's XMI of the + `render` members. The writer gave a library element an id only where the pilot's XMI of the standard library fixes one; this repository's libraries have no such norm, so a reference to one - of their elements falls back to the name as written, and the library element itself is not + of their elements fell back to the name as written, and the library element itself was not stated. Every `Element` has an `elementId` (KerML 8.3.2.1.2) and the API's JSON refers to an - element by it, so the pilot's reading is the metamodel's; + element by it, so the pilot's reading is the metamodel's — the writer now fixes an id for these + elements too; - the order of `ownedRelationship` on a usage that both redefines and has a value: the pilot lists the `Redefinition` before the `FeatureValue`, in the order the notation declares them; ours lists the value first (275 `AttributeUsage`, 97 `ReferenceUsage`, 2 `RenderingUsage`). `ownedRelationship` @@ -302,6 +303,60 @@ excerpt leaves undeclared (`perform VehicleActions::move`, `individual timeslice preflight`, `render asMyTreeDiagram` under an undefined view definition, …), which the pilot, resolving nothing, does not name. A run's own list of such movements comes from `-lost-against`. +### References into this repository's libraries link by id + +Every named element of this repository's own libraries (`DocumentQueries`, `MOSA`, `OOSEM`, …) +now carries a fixed id, derived as a standard library element's normative id is — a library +package's id is `uuid5(URL name space, "https://github.com/Open-MBEE/OpenSysML-Extensions-Library/" + +package)`, each named element under it `uuid5(package id, escaped qualified name)` — so a reference +to one is written by id, `{"@id": …}`, the element itself named by a library stub in the graph, and +never as `{"@ref": "name"}` ([the RDF mapping](../reference/rdf-mapping.md#normative-library-identity)). +Those ids are deterministic and the same on every run, but they are **not the pilot's**: no +specification fixes ids for a non-normative library, and the pilot, reading the same files as a +library directory, hashes every library package under `https://www.omg.org/spec/SysML/`, so the two +sides' ids for `MOSA::ConsensusStandard` differ by that prefix alone. The referee cannot therefore +call such a reference equal: our side writes no element for a library target (the element form +carries the model's own elements only, [above](#our-side)), so the id has no key and the comparison +is **indeterminate** — the bucket that already holds references to standard library elements — +rather than equal or different. What this measures is presence and shape: the property is stated, +as an element reference, where it was stated as a name. + +Movement on the same 920 files, pilot and keys unchanged (the exact movement a `-check` after this +change reports): present-different 50027 → 45688 (−4339), indeterminate 21034 → 25703 (+4669), +present-equal 618674 → 619268 (+594), absent-both 2102500 → 2103094 (+594), elements matched 89957 +→ 90023 (+66, pilot-only 38863 → 38797), ours-missing 1058325 → 1058259 (−66), pilot-missing +68494 → 68560 (+66); no row's present-equal count fell, and no root but `examples/spec-examples` +(−5 present-different), `tests/testdata` (−10), `examples` (−2875) and `docs/manual/examples` (−1449) +moved. By row, present-different → indeterminate, one movement per site, the end properties of a +relationship (`target`, `relatedElement`, `general`, `subsettedFeature`) moving with the end they +restate: + +| row | present-different | indeterminate | what moved | +|---|---:|---:|---| +| `Redefinition.redefinedFeature` (and `target`, `relatedElement`, `general`, `subsettedFeature`) | 388 → 58 | 542 → 938 | 330 redefinitions of a library feature (`attribute redefines caption = …`, `:>> organization = …`), plus 66 redefinitions newly matched (below); the 58 left are redefinitions the writer cannot link inside the model itself (`vehicle_b_1::providePower` redefining the feature inherited through `vehicle_b`) | +| `FeatureTyping.type` (and `target`, `relatedElement`, `general`) | 670 → 338 | 1904 → 2236 | 332 features typed by a library definition (`item ethernet : ConsensusStandard`, metadata typed by `MOSA` and `OOSEM` definitions) | +| `MetadataUsage.type` | 147 → 13 | 82 → 216 | 134 metadata usages typed by a library metadata definition (`#majorSystemComponent`, `@MOSAPackage`) | +| `ViewUsage.type` | 19 → 3 | 29 → 45 | 16 views typed by a `MOSA` or `OOSEM` view definition | +| `FeatureReferenceExpression.referent` | 99 → 71 | 436 → 464 | 28 references to a library enumeration literal (`MOSAPillar::openStandards`) | +| `Membership.memberElement` | 353 → 99 | 646 → 900 | 254 memberships of a referenced library element, the `referent` memberships among them | +| `InvocationExpression.function` | 245 → 19 | 107 → 333 | 226 calls of a calculation or function of these libraries | +| `Subclassification.superclassifier` (and `target`, `relatedElement`, `general`) | 126 → 13 | 136 → 249 | 113 definitions specializing a library definition (`:> MOSA::Standard`) | +| `PartUsage.type` | 235 → 63 | 0 → 172 | 172 parts typed by a definition of these libraries | +| `Subsetting.subsettedFeature` (and `target`, `relatedElement`, `general`) | 44 → 34 | 425 → 435 | 10 features subsetting a library feature | +| `NamespaceImport.importedNamespace`, `MembershipImport.importedMembership` | 25 → 8, 15 → 13 | 331 → 348, 265 → 267 | 17 imports of a library package, 2 of a library member | +| `CalculationUsage.type`, `ItemUsage.type`, `ReferenceUsage.type`, `ReferenceSubsetting.referencedFeature` (and its ends) | 7 → 3, 20 → 16, 48 → 46, 40 → 38 | 0 → 4, 16 → 20, 501 → 503, 24 → 26 | 4, 4, 2 and 2 references to a library element | + +The 66 newly matched elements are `Redefinition`s: a feature in a metadata usage's body that +binds a feature of the metadata definition by name (`@MOSAPackage { pillar = MOSAPillar::openStandards; }`, +which redefines `MOSAPackage::pillar`) was written as a +`ReferenceUsage` with the declared name `pillar` and a value, because its redefinition reached a +library feature the writer could not link; it is now written as the pilot writes it — unnamed, with +an owned `Redefinition` of `MOSAPackage::pillar` — so `ReferenceUsage.declaredName` falls from 70 +to 4 pilot-missing, `ReferenceUsage.ownedRedefinition`/`ownedSpecialization`/`ownedSubsetting` +gain 66 present-equal each, and the 66 `Redefinition`s match the pilot's, carrying every shape the +`Redefinition` rows already carry (`isLibraryElement`, `owningType` ours-missing; `owner` +pilot-missing; `redefinedFeature` indeterminate). + ## Launch-ready gaps The (metaclass, property) rows ranked by the corpus elements on which the pilot states a value and we state nothing (**ours-missing**), each with the derivation clause to implement and the in-flight work that already covers it, so that each row can be handed on as it stands. Where a property is an owned attribute with a default rather than a derivation, the row says so: the gap is in the writer, not in a derivation. The missing derivations are not implemented here. @@ -348,7 +403,7 @@ The (metaclass, property) rows ranked by the corpus elements on which the pilot | 38 | `FeatureValue` | `visibility` | 7048 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | owned attribute `Membership::visibility : VisibilityKind`, default `public` (KerML 8.3.2.3.5); not derived | | 39 | `FeatureTyping` | `isLibraryElement` | 5912 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`; writes `true` on library elements only, never `false`) | `Element::isLibraryElement = libraryNamespace() <> null` (KerML 8.3.2.1, deriveElementIsLibraryElement); the pilot writes `false` on every user element | | 40 | `FeatureTyping` | `owningType` | 5908 | 0 | PR #1232 (`feature/api-json-derived-properties-layered`) | `Feature::owningType = owningFeatureMembership.owningType` (KerML 8.3.3.3, deriveFeatureOwningType) | -| 41 | `Redefinition` | `redefinedFeature` | 0 | 388 | none found | `Redefinition::redefinedFeature : Feature` (KerML 8.3.3.3.8) is an element reference, and every `Element` carries an `elementId` (KerML 8.3.2.1.2) the API's JSON refers to it by; where the redefined feature is an element of this repository's libraries ours spells the name as written, `{"@ref": "caption"}`, because the writer fixes an id only for elements the pilot's standard-library XMI identifies — the same spelling as `FeatureTyping::type`, `MetadataUsage::type`, `FeatureReferenceExpression::referent` and `ReferenceSubsetting::referencedFeature` into those libraries | +| 41 | `Redefinition` | `redefinedFeature` | 0 | 58 | closed for this repository's libraries (above) | `Redefinition::redefinedFeature : Feature` (KerML 8.3.3.3.8) is an element reference, and every `Element` carries an `elementId` (KerML 8.3.2.1.2) the API's JSON refers to it by; the 330 redefinitions of a feature of this repository's libraries once spelled as a name, `{"@ref": "caption"}`, now link by the element's fixed id — the same movement for `FeatureTyping::type`, `MetadataUsage::type`, `FeatureReferenceExpression::referent` and `ReferenceSubsetting::referencedFeature` into those libraries — and the 58 left are redefinitions of a feature inherited within the model itself that the writer spells as a name (`vehicle_b_1::providePower`) | | 42 | `AttributeUsage` | `ownedRelationship` | 36 | 2323 | none found | `Element::ownedRelationship : Relationship [0..*] {ordered}` (KerML 8.3.2.1.2) in the order the notation declares them: a usage's `Redefinition` (`redefines x`) before its `FeatureValue` (`= …`); ours lists the value first | | 43 | `FeatureReferenceExpression` | `ownedMembership` | 0 | 2110 | none found | `FeatureReferenceExpression::referent` is the `memberElement` of the first `ownedMembership` that is not a `ParameterMembership` (KerML 8.3.4.8.5, deriveFeatureReferenceExpressionReferent); the pilot owns that `Membership`, ours writes the `referent` without it | diff --git a/docs/reference/api.md b/docs/reference/api.md index af5966cd16..eed58fc4ad 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1090,7 +1090,7 @@ answer. | `type` | Qualified name of the resolved type of a typed feature; absent when untyped or unresolved | | | `satisfiedRequirement` | The requirement a non-verification satisfy usage references, or the satisfy usage itself when it declares the requirement; absent when unresolved or not a satisfy usage | | | `satisfyingFeature` | The feature named by a satisfy usage's `by` clause; a feature chain (`by v.heater`) reports the feature the chain ends at; absent when there is no `by` clause or it is unresolved | | -| `elementId` | The `elementId` `Convert` writes for the element (qualified id form), read from a conversion of the model made once: a declared id as declared, a standard-library element's normative id (a library file's copy included), any other the encoding of its qualified or positional name. It is the API JSON `@id` too, except in a model of several identity scopes, whose `@id` the scope qualifies. Absent for a library element a model gives an id of its own, which `Convert` references by name. A query reading it of a model `Convert` refuses is `FAILED_PRECONDITION`. Reported only when a query selects or constrains it, never by an empty `select`, since reading it converts the model | | +| `elementId` | The `elementId` `Convert` writes for the element (qualified id form), read from a conversion of the model made once: a declared id as declared, a bundled library element's fixed id — a standard-library element's normative id, an OpenSysML extension's extension id (a library file's copy included) — any other the encoding of its qualified or positional name. It is the API JSON `@id` too, except in a model of several identity scopes, whose `@id` the scope qualifies. Absent for a library element a model gives an id of its own, which `Convert` references by name. A query reading it of a model `Convert` refuses is `FAILED_PRECONDITION`. Reported only when a query selects or constrains it, never by an empty `select`, since reading it converts the model | | | `multiplicityLower` | Declared lower bound | ✅ | | `multiplicityUpper` | Declared upper bound, `*` when unbounded | ✅ | diff --git a/docs/reference/rdf-mapping.md b/docs/reference/rdf-mapping.md index d22cfc19c2..14416cce5b 100644 --- a/docs/reference/rdf-mapping.md +++ b/docs/reference/rdf-mapping.md @@ -251,6 +251,22 @@ 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. +**OpenSysML's own libraries have fixed ids too.** The bundled extension +libraries (`OpenSysML Libraries/`: `DocumentQueries`, `MOSA`, `OOSEM`, …) are +not normative — no specification fixes ids for them — so this implementation +fixes them the same way, deterministic and the same on every run: a library +package's id is `uuid5(URL name space, +"https://github.com/Open-MBEE/OpenSysML-Extensions-Library/" + package name)`, +and every named element under it is `uuid5(package id, escaped qualified name)`, +as a normative id is under the OMG specification URL. A reference into one (an +`#consensusStandard` metadata usage, a `redefines caption`) therefore links by +id — `elmt:` in Turtle, `{"@id": …}` in the element form — with the element +named by a library stub as below, instead of carrying the name as text. These +are **extension** ids, not normative ones: hover says `(extension, OpenSysML)`, +and they are not the ids the OMG pilot mints when it loads the same files as a +library directory (it hashes any library under the SysML specification URL), so +no specification makes two tools agree on them; only the derivation above is fixed. + **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 diff --git a/internal/frontend/lsp/hover.go b/internal/frontend/lsp/hover.go index a88843c56a..c34818bc34 100644 --- a/internal/frontend/lsp/hover.go +++ b/internal/frontend/lsp/hover.go @@ -82,7 +82,7 @@ func (s *Server) identityLine(doc string, sym *symbols.Symbol) string { return "" } provenance := info.Source.String() - if info.Normative() { + if info.Fixed() { provenance += ", " + info.Language.String() } return fmt.Sprintf("Element id `%s` (%s)", info.EffectiveID, provenance) diff --git a/internal/frontend/lsp/hover_test.go b/internal/frontend/lsp/hover_test.go index 7c96d662f0..5b84102ff9 100644 --- a/internal/frontend/lsp/hover_test.go +++ b/internal/frontend/lsp/hover_test.go @@ -8,6 +8,7 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" + "github.com/Open-MBEE/OpenSysML/internal/semantic/identity" "github.com/Open-MBEE/OpenSysML/internal/workspace/model" ) @@ -334,14 +335,14 @@ func TestHoverBindingConnectorEnds(t *testing.T) { } } -// An id fixed by an annotation or by the norm is stated; a derived id, which is -// just the encoded name, is not. +// An id fixed by an annotation, by the norm or by one of OpenSysML's own +// libraries is stated; a derived id, which is just the encoded name, is not. func TestHoverStatesDeclaredAndNormativeIdentity(t *testing.T) { ws := model.NewWorkspace() s := NewServer(ws) initMarkdownHover(t, s) name := uri.File("/tmp/hid.sysml").Filename() - src := "package P {\n part def Wheel {\n @IdentityMetadata::ElementId { id = \"wheel-id\"; }\n }\n attribute mass : ScalarValues::Real;\n part w : Wheel;\n}\n" + src := "package P {\n part def Wheel {\n @IdentityMetadata::ElementId { id = \"wheel-id\"; }\n }\n attribute mass : ScalarValues::Real;\n item std : MOSA::ConsensusStandard;\n part w : Wheel;\n}\n" ws.Open(name, []byte(src), 1) for _, tc := range []struct { @@ -350,6 +351,7 @@ func TestHoverStatesDeclaredAndNormativeIdentity(t *testing.T) { }{ {at: "Wheel {", want: "\n\nElement id `wheel-id` (declared)"}, {at: "Real;", want: "\n\nElement id `14c0aa22-5489-59b5-b438-ded26e83ba31` (normative, KerML)"}, + {at: "ConsensusStandard;", want: "\n\nElement id `" + identity.ElementID(identity.OpenSysML, "MOSA::ConsensusStandard") + "` (extension, OpenSysML)"}, } { res := hoverInSrc(t, s, name, src, strings.Index(src, tc.at)) if !strings.HasSuffix(res.Contents.Value, tc.want) { diff --git a/internal/frontend/lsp/identity.go b/internal/frontend/lsp/identity.go index 1d405f60d7..61ffa178be 100644 --- a/internal/frontend/lsp/identity.go +++ b/internal/frontend/lsp/identity.go @@ -46,7 +46,7 @@ func (s *Server) identityActions(name string, doc *model.Document, want source.S Edit: workspaceEdit(uri, positionsOf(doc), annotate(doc.Content, []annotation{projectRef(root)})), }) } - if info.Annotated || info.Normative() { + if info.Annotated || info.Fixed() { return out, nil } id, err := reposync.MintUUID() diff --git a/internal/semantic/identity/identity.go b/internal/semantic/identity/identity.go index 338acbc24b..c5af677910 100644 --- a/internal/semantic/identity/identity.go +++ b/internal/semantic/identity/identity.go @@ -93,6 +93,9 @@ const ( SourceNormative // SourceDeclared is the id an ElementId annotation declares. SourceDeclared + // SourceExtension is the UUID this implementation fixes for a named element + // of its own extension libraries, derived as a normative id is under their URL. + SourceExtension ) func (s Source) String() string { @@ -101,22 +104,27 @@ func (s Source) String() string { return "normative" case SourceDeclared: return "declared" + case SourceExtension: + return "extension" } return "derived" } +// Fixed reports an id a bundled library fixes: normative or extension. +func (s Source) Fixed() bool { return s == SourceNormative || s == SourceExtension } + // Info is one symbol's identity: its effective id, where it came from, and // the project scope it resolves against. type Info struct { Symbol *symbols.Symbol // FQN is the symbol's qualified name, which the derived id encodes. FQN string - // EffectiveID is the annotated id when declared, the normative id of a - // standard library element otherwise, the derived id failing both. + // EffectiveID is the annotated id when declared, the fixed id of a bundled + // library element otherwise, the derived id failing both. EffectiveID string - // Source is which of the three EffectiveID is. + // Source is which of the four EffectiveID is. Source Source - // Language is the library half a normative id is minted under; zero otherwise. + // Language is the library a fixed id is minted under; zero otherwise. Language Language // Annotated reports an ElementId annotation on the element. Annotated bool @@ -137,10 +145,14 @@ type Info struct { // Normative reports an effective id the norm fixes for the element. func (i *Info) Normative() bool { return i.Source == SourceNormative } -// OwningMembershipID is the normative id of the membership owning a standard -// library element; "" for any other, whose membership id derives from its own. +// Fixed reports an effective id a bundled library fixes for the element: the +// norm's for a standard library element, this implementation's for an extension's. +func (i *Info) Fixed() bool { return i.Source.Fixed() } + +// OwningMembershipID is the fixed id of the membership owning a bundled library +// element; "" for any other, whose membership id derives from its own. func (i *Info) OwningMembershipID() string { - if !i.Normative() { + if !i.Fixed() { return "" } return OwningMembershipID(i.Language, i.FQN) @@ -245,6 +257,9 @@ func (b *builder) infoOf(sym *symbols.Symbol) *Info { info := &Info{Symbol: sym, FQN: fqn, EffectiveID: rdf.EncodeElementID(fqn)} if lang, ok := b.norm.language(sym); ok { info.Source, info.Language = SourceNormative, lang + if lang == OpenSysML { + info.Source = SourceExtension + } info.EffectiveID = ElementID(lang, fqn) } for _, site := range b.model.AnnotationSitesOf(sym) { @@ -268,8 +283,8 @@ func (b *builder) infoOf(sym *symbols.Symbol) *Info { if !d.Declared || d.ID == "" { continue } - // An annotation restating the norm's id declares nothing the norm does not. - if d.ID != info.EffectiveID || !info.Normative() { + // An annotation restating the library's fixed id declares nothing it does not. + if d.ID != info.EffectiveID || !info.Fixed() { info.EffectiveID = d.ID info.Source, info.Language = SourceDeclared, 0 } diff --git a/internal/semantic/identity/library.go b/internal/semantic/identity/library.go index 6e40f6852c..6ce39bc20e 100644 --- a/internal/semantic/identity/library.go +++ b/internal/semantic/identity/library.go @@ -7,19 +7,22 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/syntax/source" ) -// LibraryLanguage is the half of the standard library a bundled tier belongs to: -// Kernel is KerML, Systems and Domain are SysML; extensions and workspace are neither. +// LibraryLanguage is the library a bundled tier's ids are fixed under: Kernel +// is KerML, Systems and Domain are SysML, this implementation's extensions are +// OpenSysML; an unplaced library tier and the workspace are none. func LibraryLanguage(tier symbols.LibraryTier) (Language, bool) { switch tier { case symbols.TierKernelSemantic, symbols.TierKernelDataType, symbols.TierKernelFunction: return KerML, true case symbols.TierSystems, symbols.TierDomain: return SysML, true + case symbols.TierOpenSysML: + return OpenSysML, true } return 0, false } -// qualifier decides which library symbols the norm gives a qualified name, and so an id. +// qualifier decides which library symbols their library gives a qualified name, and so an id. type qualifier struct { idx libraryView memo map[*symbols.Symbol]bool @@ -29,7 +32,7 @@ func newQualifier(idx libraryView) *qualifier { return &qualifier{idx: idx, memo: make(map[*symbols.Symbol]bool)} } -// language is the language whose norm fixes sym's id, if any. +// language is the library whose scheme fixes sym's id, if any. func (q *qualifier) language(sym *symbols.Symbol) (Language, bool) { lang, ok := LibraryLanguage(q.idx.LibraryTier(sym)) if !ok || !q.qualified(sym) { @@ -68,8 +71,9 @@ func firstSoNamed(sym *symbols.Symbol) bool { return false } -// LibraryElement is one named element of the standard library and the ids the -// norm fixes for it and for the membership that owns it. +// LibraryElement is one named element of a bundled library and the ids fixed +// for it and for the membership that owns it: the norm's for the standard +// library, this implementation's for its extension libraries. type LibraryElement struct { Symbol *symbols.Symbol FQN string @@ -240,7 +244,7 @@ func newCatalog() *Catalog { } } -// buildCatalog fixes the id of every element the norm names in idx's library documents. +// buildCatalog fixes the id of every named element of idx's library documents. func buildCatalog(idx libraryView) *Catalog { c := newCatalog() q := newQualifier(idx) diff --git a/internal/semantic/identity/normative.go b/internal/semantic/identity/normative.go index 53a60e87ef..fafe282476 100644 --- a/internal/semantic/identity/normative.go +++ b/internal/semantic/identity/normative.go @@ -22,15 +22,26 @@ const ( KerML Language = iota + 1 // SysML is the Systems and Domain libraries. SysML + // OpenSysML is this implementation's own extension libraries, whose ids it + // fixes the same way under their upstream URL. + OpenSysML ) -// Prefix is the specification URL the language's library package ids hash under. +// ExtensionPrefix is the URL the OpenSysML extension-library package ids hash +// under: the libraries' upstream home, as the OMG libraries hash under their +// specification URL. Not the pilot's choice: loaded as a library directory, the +// pilot hashes any library under the SysML specification URL. +const ExtensionPrefix = "https://github.com/Open-MBEE/OpenSysML-Extensions-Library/" + +// Prefix is the URL the language's library package ids hash under. func (l Language) Prefix() string { switch l { case KerML: return "https://www.omg.org/spec/KerML/" case SysML: return "https://www.omg.org/spec/SysML/" + case OpenSysML: + return ExtensionPrefix } return "" } @@ -41,6 +52,8 @@ func (l Language) String() string { return "KerML" case SysML: return "SysML" + case OpenSysML: + return "OpenSysML" } return "unknown" } @@ -75,8 +88,9 @@ const Separator = "::" // owningMembershipPath is the path suffix an owning membership's id hashes. const owningMembershipPath = "/owningMembership" -// ElementID is the normative id of the library element with the given qualified +// ElementID is the fixed id of the library element with the given qualified // name (segments as declared, the first its library package); "" for none. +// Normative for a KerML or SysML element, this implementation's for an OpenSysML one. func ElementID(lang Language, qualifiedName string) string { pkg, path, top, ok := split(lang, qualifiedName) if !ok { @@ -88,7 +102,7 @@ func ElementID(lang Language, qualifiedName string) string { return uuid5(pkg, path).String() } -// OwningMembershipID is the normative id of the OwningMembership owning the +// OwningMembershipID is the fixed id of the OwningMembership owning the // library element with the given qualified name, a library package included. func OwningMembershipID(lang Language, qualifiedName string) string { pkg, path, _, ok := split(lang, qualifiedName) diff --git a/internal/translate/export/library_names.go b/internal/translate/export/library_names.go index 7fae3ebce9..56f09e19aa 100644 --- a/internal/translate/export/library_names.go +++ b/internal/translate/export/library_names.go @@ -108,7 +108,7 @@ func encodeLibraryNames(doc string) any { } names := make(map[string]string) for node, el := range e.ids.byNode { - if el.source != identity.SourceNormative { + if !el.source.Fixed() { continue } if fqn, ok := e.fqn[node]; ok { diff --git a/internal/translate/export/rdf_identity.go b/internal/translate/export/rdf_identity.go index f7e1958b1c..125d4f14b8 100644 --- a/internal/translate/export/rdf_identity.go +++ b/internal/translate/export/rdf_identity.go @@ -231,8 +231,8 @@ func idByte(c byte) bool { return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-' } -// libraryName reports whether fqn names a standard library element the norm -// fixes an id for, so a reference to it links rather than carries text. +// libraryName reports whether fqn names a bundled library element with a fixed +// id, so a reference to it links rather than carries text. func (f *identityFacts) libraryName(fqn string) bool { if _, declared := f.byFQN[fqn]; declared { return true @@ -241,8 +241,8 @@ func (f *identityFacts) libraryName(fqn string) bool { return ok } -// libraryElement records the normative identity of a bundled library symbol on -// first sight and returns its qualified name; false where the norm fixes no id. +// libraryElement records the fixed identity of a bundled library symbol on +// first sight and returns its qualified name; false where its library fixes no id. func (f *identityFacts) libraryElement(sym *symbols.Symbol) (string, bool) { if fqn, seen := f.library[sym]; seen { return fqn, fqn != "" @@ -252,7 +252,7 @@ func (f *identityFacts) libraryElement(sym *symbols.Symbol) (string, bool) { return "", false } info, ok := identity.Of(f.model, f.res, sym) - if !ok || info.Source != identity.SourceNormative { + if !ok || !info.Fixed() { return "", false } if _, taken := f.byFQN[info.FQN]; taken { diff --git a/internal/translate/export/rdf_out.go b/internal/translate/export/rdf_out.go index 98cd52d256..17b43f055f 100644 --- a/internal/translate/export/rdf_out.go +++ b/internal/translate/export/rdf_out.go @@ -2738,22 +2738,22 @@ func (e *encoder) symbolTerm(target *symbols.Symbol) rdf.Term { if decl, fqn, ok := e.linked(target, true); ok { return e.ids.subjectForNode(decl, fqn) } - // The member's own identity already claims a normative target's qualified + // The member's own identity already claims a library target's qualified // name, which linked refuses to repeat; name its IRI as a reference does. info, ok := identity.Of(e.ids.model, e.res, target) - if !ok || info.Source != identity.SourceNormative { + if !ok || !info.Fixed() { return rdf.Term{} } el := elementIdentity{id: info.EffectiveID, source: info.Source, membership: info.OwningMembershipID()} return e.ids.subjectOf(el, info.FQN) } -// normativeFQN is the qualified name a symbol carries in the norm, or "" for -// any other element: linked hides a library element whose qualified name the -// element stating it already claimed, which this answers anyway. +// normativeFQN is the qualified name a symbol carries in its library's fixed +// scheme, or "" for any other element: linked hides a library element whose +// qualified name the element stating it already claimed, which this answers anyway. func (e *encoder) normativeFQN(sym *symbols.Symbol) string { info, ok := identity.Of(e.ids.model, e.res, sym) - if !ok || info.Source != identity.SourceNormative { + if !ok || !info.Fixed() { return "" } return info.FQN diff --git a/tests/export/extension_library_identity_test.go b/tests/export/extension_library_identity_test.go new file mode 100644 index 0000000000..710955e92f --- /dev/null +++ b/tests/export/extension_library_identity_test.go @@ -0,0 +1,118 @@ +package export_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/identity" + "github.com/Open-MBEE/OpenSysML/internal/translate/convert" + "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" +) + +const extensionLibraryModel = `package Radios { + private import MOSA::*; + #consensusStandard item ethernet : ConsensusStandard { + :>> organization = "IEEE"; + } + #majorSystemComponent part radio; +} +` + +// A reference into one of OpenSysML's own libraries links by the element's +// fixed extension id — never a {"@ref": name} — and the Turtle names the +// element under that id as it names a standard library element. +func TestReferencesIntoOpenSysMLLibrariesLinkByFixedID(t *testing.T) { + catalog := identity.LibraryCatalog(libs.NewModelIndex()) + idOf := func(fqn string) string { + t.Helper() + el, ok := catalog.ElementNamed(fqn) + if !ok || el.Language != identity.OpenSysML || el.ID != identity.ElementID(identity.OpenSysML, fqn) { + t.Fatalf("%s: catalogued as %+v, want a fixed OpenSysML id", fqn, el) + } + return el.ID + } + out, err := convert.Convert("r.sysml", []byte(extensionLibraryModel), convert.FormatSysML, convert.FormatAPIJSON) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(out), `"@ref"`) { + t.Fatalf("the element form still references a library element by name:\n%s", out) + } + var elements []map[string]any + if err := json.Unmarshal(out, &elements); err != nil { + t.Fatal(err) + } + refs := map[string][]string{} // metaclass.property → referenced ids + for _, e := range elements { + metaclass, _ := e["@type"].(string) + for _, property := range []string{"redefinedFeature", "type", "importedNamespace"} { + switch v := e[property].(type) { + case map[string]any: + refs[metaclass+"."+property] = append(refs[metaclass+"."+property], v["@id"].(string)) + case []any: + for _, item := range v { + refs[metaclass+"."+property] = append(refs[metaclass+"."+property], item.(map[string]any)["@id"].(string)) + } + } + } + } + for key, want := range map[string]string{ + "Redefinition.redefinedFeature": idOf("MOSA::Standard::organization"), + "FeatureTyping.type": idOf("MOSA::ConsensusStandard"), + "MetadataUsage.type": idOf("MOSA::MajorSystemComponentMetadata"), + "NamespaceImport.importedNamespace": idOf("MOSA"), + } { + found := false + for _, id := range refs[key] { + found = found || id == want + } + if !found { + t.Errorf("%s: references %v, want %s among them", key, refs[key], want) + } + } + + turtle, err := convert.Convert("r.sysml", []byte(extensionLibraryModel), convert.FormatSysML, convert.FormatTurtle) + if err != nil { + t.Fatal(err) + } + organization := idOf("MOSA::Standard::organization") + stub := "elmt:" + organization + "\n" + i := strings.Index(string(turtle), stub) + if i < 0 { + t.Fatalf("the graph does not name the redefined library feature %s", organization) + } + block := string(turtle)[i:] + block = block[:strings.Index(block, " .\n")] + for _, want := range []string{ + `sysml:elementId "` + organization + `"`, + `sysml:qualifiedName "MOSA::Standard::organization"`, + `sysml:isLibraryElement "true"^^xsd:boolean`, + } { + if !strings.Contains(block, want) { + t.Errorf("the library stub lacks %s:\n%s", want, block) + } + } + if strings.Contains(string(turtle), `sysml:redefinedFeature "organization"`) { + t.Error("the graph still carries the redefined library feature as text") + } + + for _, tc := range []struct { + name string + in []byte + from convert.Format + }{ + {"turtle", turtle, convert.FormatTurtle}, + {"api-json", out, convert.FormatAPIJSON}, + } { + back, err := convert.Convert("r."+tc.name, tc.in, tc.from, convert.FormatSysML) + if err != nil { + t.Fatalf("%s back to notation: %v", tc.name, err) + } + for _, want := range []string{"#consensusStandard item ethernet : ConsensusStandard", ":>> organization = \"IEEE\";", "#majorSystemComponent part radio;"} { + if !strings.Contains(string(back), want) { + t.Errorf("%s back to notation lacks %q:\n%s", tc.name, want, back) + } + } + } +} diff --git a/tests/identity/identity_test.go b/tests/identity/identity_test.go index 518bb5e22d..a2426c2ea8 100644 --- a/tests/identity/identity_test.go +++ b/tests/identity/identity_test.go @@ -319,22 +319,68 @@ func TestSystemsLibraryElementIsMintedUnderSysML(t *testing.T) { } } -func TestUserElementIsDerivedAndOpenSysMLLibraryIsNotNormative(t *testing.T) { +func TestUserElementIsDerived(t *testing.T) { table, idx := buildTable(t, `package Vehicles { part def Vehicle; }`) info := infoOf(t, table, idx, "Vehicles::Vehicle") - if info.Source != identity.SourceDerived || info.Normative() || info.OwningMembershipID() != "" { + if info.Source != identity.SourceDerived || info.Normative() || info.Fixed() || info.OwningMembershipID() != "" { t.Fatalf("Vehicles::Vehicle: source=%v, want derived", info.Source) } +} + +// libraryInfo is the identity of a bundled library element idx holds, read as +// a model referencing it would. +func libraryInfo(t *testing.T, idx *symbols.Index, fqn string) *identity.Info { + t.Helper() res := resolve.New(idx) model := semantics.NewModel(res) res.SetModel(model) - syms := idx.LookupQualified("IdentityMetadata::ElementId") + syms := idx.LookupQualified(fqn) if len(syms) == 0 { - t.Fatal("IdentityMetadata::ElementId: no symbol") + t.Fatalf("%s: no symbol", fqn) + } + info, ok := identity.Of(model, res, syms[0]) + if !ok { + t.Fatalf("%s: no identity", fqn) + } + return info +} + +// An element of one of OpenSysML's own libraries is not normative — no +// specification fixes its id — but it carries a fixed id all the same, derived +// as a normative one is under the extension libraries' URL, so a reference to +// it links by id rather than by name, the same on every run. +func TestOpenSysMLLibraryElementCarriesAFixedExtensionID(t *testing.T) { + const fqn = "IdentityMetadata::ElementId" + _, idx := buildTable(t, `package Vehicles { part def Vehicle; }`) + ext := libraryInfo(t, idx, fqn) + want := identity.ElementID(identity.OpenSysML, fqn) + if want == "" || ext.Source != identity.SourceExtension || ext.Normative() || !ext.Fixed() || + ext.Language != identity.OpenSysML || ext.EffectiveID != want || + ext.OwningMembershipID() != identity.OwningMembershipID(identity.OpenSysML, fqn) { + t.Fatalf("%s: %+v, want extension id %s", fqn, ext, want) + } + if ext.Source.String() != "extension" || ext.Language.String() != "OpenSysML" { + t.Fatalf("%s: spelled %s, %s", fqn, ext.Source, ext.Language) + } + // The scheme: the library package hashes under the extension libraries' + // URL, its elements under the package; never under the OMG specification URLs. + if identity.ElementID(identity.OpenSysML, "IdentityMetadata") != identity.NamespaceOf(identity.ExtensionPrefix+"IdentityMetadata") { + t.Fatal("IdentityMetadata: the package id is not uuid5(URL, ExtensionPrefix+name)") + } + if want == identity.ElementID(identity.SysML, fqn) || want == identity.ElementID(identity.KerML, fqn) { + t.Fatalf("%s: the extension id %s is a specification-URL id", fqn, want) + } + // Deterministic: an independent analysis derives the same id. + _, again := buildTable(t, `package Other { part def Thing; }`) + if other := libraryInfo(t, again, fqn); other.EffectiveID != want { + t.Fatalf("%s: %s on one analysis, %s on another", fqn, want, other.EffectiveID) + } + catalog := identity.LibraryCatalog(idx) + if el, ok := catalog.ElementNamed(fqn); !ok || el.ID != want || el.Language != identity.OpenSysML { + t.Fatalf("%s: catalogued as %+v, want %s", fqn, el, want) } - ext, ok := identity.Of(model, res, syms[0]) - if !ok || ext.Source != identity.SourceDerived { - t.Fatalf("IdentityMetadata::ElementId: source=%v, want derived (an OpenSysML extension is not normative)", ext.Source) + if el, ok := catalog.Element(want); !ok || el.FQN != fqn { + t.Fatalf("%s: the catalog does not resolve %s", fqn, want) } } diff --git a/tests/identity/pilot_library_xmi_test.go b/tests/identity/pilot_library_xmi_test.go index 29f7e59405..0d6b6b6f44 100644 --- a/tests/identity/pilot_library_xmi_test.go +++ b/tests/identity/pilot_library_xmi_test.go @@ -113,6 +113,14 @@ func TestPilotLibraryXMI(t *testing.T) { derived := map[string]bool{} seenErrataOnly := 0 for _, el := range ours { + if el.Language == identity.OpenSysML { + // An extension's fixed id is this implementation's, not the norm's: the + // pilot's XMI must know nothing under it. + if _, collides := lib.byID[el.ID]; collides { + wrong = append(wrong, fmt.Sprintf("%s: the extension id %s is a pilot library element's", el.FQN, el.ID)) + } + continue + } derived[el.ID] = true node, ok := lib.byID[el.ID] errataOnly, listed := errataOnlyElements[el.FQN]