diff --git a/api/proto/sysml.pb.go b/api/proto/sysml.pb.go index c8d651e34a..8aa5d5f531 100644 --- a/api/proto/sysml.pb.go +++ b/api/proto/sysml.pb.go @@ -3723,11 +3723,14 @@ type ConvertRequest struct { // *ConvertRequest_Content // *ConvertRequest_ModelHash Source isConvertRequest_Source `protobuf_oneof:"source"` - // "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from - // file_path's extension, and is notation for a model_hash, since that is what - // parse reads; inline content has neither, so it must say. + // "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or + // "mdzip" for a SysML v1 model, which is read and migrated to v2 and never + // written. Empty infers from file_path's extension, and is notation for a + // model_hash, since that is what parse reads; inline content has neither, so + // it must say. FromFormat string `protobuf:"bytes,3,opt,name=from_format,json=fromFormat,proto3" json:"from_format,omitempty"` - // Format to write, named as in from_format. Empty is rejected. + // Format to write, named as in from_format; the v1 names are refused, since + // a v2 model has no v1 form. Empty is rejected. ToFormat string `protobuf:"bytes,4,opt,name=to_format,json=toFormat,proto3" json:"to_format,omitempty"` // Write notation back out even when the parser could not read all of it, // reporting its syntax errors as diagnostics. Notation to notation only: @@ -3860,7 +3863,8 @@ type ConvertResponse struct { // Set when either format is RDF, whose mapping is experimental: it covers // model structure and the behavior its bodies state, refuses what it cannot // write back, and its vocabulary may change without a compatibility path. - // Notation to notation is stable and leaves this unset. + // Also set when the source is SysML v1, whose migration is experimental in + // the same sense. Notation to notation is stable and leaves this unset. Experimental bool `protobuf:"varint,6,opt,name=experimental,proto3" json:"experimental,omitempty"` // What is experimental about the conversion, in the wording every surface // reports it in. Empty when experimental is false. diff --git a/api/proto/sysml.proto b/api/proto/sysml.proto index a400763455..d656ffff7a 100644 --- a/api/proto/sysml.proto +++ b/api/proto/sysml.proto @@ -759,11 +759,14 @@ message ConvertRequest { string content = 2; string model_hash = 6; } - // "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from - // file_path's extension, and is notation for a model_hash, since that is what - // parse reads; inline content has neither, so it must say. + // "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or + // "mdzip" for a SysML v1 model, which is read and migrated to v2 and never + // written. Empty infers from file_path's extension, and is notation for a + // model_hash, since that is what parse reads; inline content has neither, so + // it must say. string from_format = 3; - // Format to write, named as in from_format. Empty is rejected. + // Format to write, named as in from_format; the v1 names are refused, since + // a v2 model has no v1 form. Empty is rejected. string to_format = 4; // Write notation back out even when the parser could not read all of it, // reporting its syntax errors as diagnostics. Notation to notation only: @@ -786,7 +789,8 @@ message ConvertResponse { // Set when either format is RDF, whose mapping is experimental: it covers // model structure and the behavior its bodies state, refuses what it cannot // write back, and its vocabulary may change without a compatibility path. - // Notation to notation is stable and leaves this unset. + // Also set when the source is SysML v1, whose migration is experimental in + // the same sense. Notation to notation is stable and leaves this unset. bool experimental = 6; // What is experimental about the conversion, in the wording every surface // reports it in. Empty when experimental is false. diff --git a/changes/unreleased/sysml-v1-migration-docs.added.md b/changes/unreleased/sysml-v1-migration-docs.added.md new file mode 100644 index 0000000000..6185a617ce --- /dev/null +++ b/changes/unreleased/sysml-v1-migration-docs.added.md @@ -0,0 +1,2 @@ +- **SysML v1 migration is documented on every surface it reaches.** `ConvertRequest.from_format` and `ConvertResponse.experimental` in `api/proto/sysml.proto`, `docs/reference/wire-contract.md` and `docs/reference/api.md` now name `xmi`, `uml` and `mdzip` as input-only formats, the extensions they are inferred from, the canonical `xmi` a response answers, and that a migration marks the response experimental as an RDF conversion does; the Python client's `convert` docstrings and guide chapter 9 say the same. Guide chapter 11 walks one migration through — running it with `-migration-report`, reading the four verdicts, finishing units, behaviors and operations by hand, and checking the result as a v2 model — and the roadmap records the migration's open items: behaviors, operations and receptions, units and quantity kinds, the report over gRPC, and stable identity for a re-migration. +- **The Go client names SysML v1 as a conversion source.** `opensysml.FormatXMI` is the format a `Conversion` answers when `ConvertFile` read UML XMI, an Eclipse UML2 `.uml` file or a `.mdzip` archive; the `Conversion` is `Experimental` with the migration notice, and asking to write `FormatXMI` is refused with `CodeInvalidArgument`. diff --git a/client/opensysml/README.md b/client/opensysml/README.md index 6c22000f30..8afee96534 100644 --- a/client/opensysml/README.md +++ b/client/opensysml/README.md @@ -38,7 +38,7 @@ inst, err := client.Instantiate(ctx, model, "Demo::Vehicle") | Choose who answers | `ListEngines`, `WithEngine`, `Engine`, `CalcEngine` | | Search it | `Query`, `QueryOSLC` | | Report on it | `RunDocumentQuery`, `RenderDocument` | -| Write it out | `Convert`, `ConvertFile`, `ConvertSource` | +| Write it out, or migrate a SysML v1 model in | `Convert`, `ConvertFile`, `ConvertSource` | | Change its source | `ApplyEdits` | Execution and verification take the same handles the rest of the API takes: diff --git a/client/opensysml/authoring.go b/client/opensysml/authoring.go index afa7cde301..ceb76e0a66 100644 --- a/client/opensysml/authoring.go +++ b/client/opensysml/authoring.go @@ -10,10 +10,10 @@ import ( // Format is a representation a model is written in or read from. type Format string -// The formats conversion accepts. There are two canonical ones, FormatSysML and -// FormatTTL, and a Conversion answers by those names whichever alias was asked -// for. RDF, in any spelling, is an experimental mapping, which a Conversion -// reports. +// The formats conversion accepts. There are two canonical ones that are written, +// FormatSysML and FormatTTL, and a Conversion answers by those names whichever +// alias was asked for. RDF, in any spelling, is an experimental mapping, which a +// Conversion reports; so is migration from FormatXMI, which is only ever read. const ( FormatSysML Format = "sysml" FormatTTL Format = "ttl" @@ -25,6 +25,9 @@ const ( // serialization written. FormatTurtle Format = "turtle" FormatRDF Format = "rdf" + // FormatXMI is SysML v1 as UML XMI, an Eclipse UML2 .uml file or a .mdzip + // archive, migrated to v2 on the way in. Asking to write it is refused. + FormatXMI Format = "xmi" ) // ConvertOption configures Convert and ConvertFile. @@ -59,7 +62,7 @@ type Conversion struct { From Format To Format // Experimental is set when either format is RDF, whose vocabulary may change - // without a compatibility path. + // without a compatibility path, or the source is SysML v1, whose migration may. Experimental bool // ExperimentalNotice says what is experimental about the conversion, empty // when it is not. diff --git a/client/opensysml/surface_test.go b/client/opensysml/surface_test.go index d0795bd6a1..dd91b472cd 100644 --- a/client/opensysml/surface_test.go +++ b/client/opensysml/surface_test.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" "net/http/httptest" + "path/filepath" "reflect" "strings" "testing" @@ -676,6 +677,29 @@ func TestAFormatAliasIsAnsweredCanonically(t *testing.T) { } } +func TestConvertFileMigratesSysMLv1(t *testing.T) { + client := newClient(t) + xmi := filepath.Join("..", "..", "internal", "core", "migrate", "testdata", "xmi", "vehicle.xmi") + conversion, err := client.ConvertFile(context.Background(), xmi, opensysml.FormatSysML) + if err != nil { + t.Fatalf("ConvertFile: %v", err) + } + if conversion.From != opensysml.FormatXMI || conversion.To != opensysml.FormatSysML { + t.Errorf("conversion = %s to %s, want xmi to sysml", conversion.From, conversion.To) + } + if !strings.Contains(conversion.Content, "part def Vehicle") { + t.Errorf("conversion does not carry the migrated model:\n%s", conversion.Content) + } + if !conversion.Experimental || !strings.Contains(conversion.ExperimentalNotice, "SysML v1 migration") { + t.Errorf("a migration does not report itself as experimental: %q", conversion.ExperimentalNotice) + } + + _, err = client.ConvertFile(context.Background(), xmi, opensysml.FormatXMI) + if !errors.Is(err, opensysml.CodeInvalidArgument) { + t.Errorf("writing xmi: err = %v, want CodeInvalidArgument", err) + } +} + func TestConvertSourceReadsInlineContent(t *testing.T) { client := newClient(t) conversion, err := client.ConvertSource(context.Background(), editableSource, opensysml.FormatSysML, diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertRequest.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertRequest.java index 9d5fe0599e..8c9f93dc17 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertRequest.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertRequest.java @@ -256,9 +256,11 @@ public java.lang.String getModelHash() { private volatile java.lang.Object fromFormat_ = ""; /** *
-   * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from
-   * file_path's extension, and is notation for a model_hash, since that is what
-   * parse reads; inline content has neither, so it must say.
+   * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or
+   * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never
+   * written. Empty infers from file_path's extension, and is notation for a
+   * model_hash, since that is what parse reads; inline content has neither, so
+   * it must say.
    * 
* * string from_format = 3 [json_name = "fromFormat"]; @@ -279,9 +281,11 @@ public java.lang.String getFromFormat() { } /** *
-   * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from
-   * file_path's extension, and is notation for a model_hash, since that is what
-   * parse reads; inline content has neither, so it must say.
+   * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or
+   * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never
+   * written. Empty infers from file_path's extension, and is notation for a
+   * model_hash, since that is what parse reads; inline content has neither, so
+   * it must say.
    * 
* * string from_format = 3 [json_name = "fromFormat"]; @@ -307,7 +311,8 @@ public java.lang.String getFromFormat() { private volatile java.lang.Object toFormat_ = ""; /** *
-   * Format to write, named as in from_format. Empty is rejected.
+   * Format to write, named as in from_format; the v1 names are refused, since
+   * a v2 model has no v1 form. Empty is rejected.
    * 
* * string to_format = 4 [json_name = "toFormat"]; @@ -328,7 +333,8 @@ public java.lang.String getToFormat() { } /** *
-   * Format to write, named as in from_format. Empty is rejected.
+   * Format to write, named as in from_format; the v1 names are refused, since
+   * a v2 model has no v1 form. Empty is rejected.
    * 
* * string to_format = 4 [json_name = "toFormat"]; @@ -1112,9 +1118,11 @@ public Builder setModelHashBytes( private java.lang.Object fromFormat_ = ""; /** *
-     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from
-     * file_path's extension, and is notation for a model_hash, since that is what
-     * parse reads; inline content has neither, so it must say.
+     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or
+     * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never
+     * written. Empty infers from file_path's extension, and is notation for a
+     * model_hash, since that is what parse reads; inline content has neither, so
+     * it must say.
      * 
* * string from_format = 3 [json_name = "fromFormat"]; @@ -1134,9 +1142,11 @@ public java.lang.String getFromFormat() { } /** *
-     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from
-     * file_path's extension, and is notation for a model_hash, since that is what
-     * parse reads; inline content has neither, so it must say.
+     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or
+     * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never
+     * written. Empty infers from file_path's extension, and is notation for a
+     * model_hash, since that is what parse reads; inline content has neither, so
+     * it must say.
      * 
* * string from_format = 3 [json_name = "fromFormat"]; @@ -1157,9 +1167,11 @@ public java.lang.String getFromFormat() { } /** *
-     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from
-     * file_path's extension, and is notation for a model_hash, since that is what
-     * parse reads; inline content has neither, so it must say.
+     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or
+     * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never
+     * written. Empty infers from file_path's extension, and is notation for a
+     * model_hash, since that is what parse reads; inline content has neither, so
+     * it must say.
      * 
* * string from_format = 3 [json_name = "fromFormat"]; @@ -1176,9 +1188,11 @@ public Builder setFromFormat( } /** *
-     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from
-     * file_path's extension, and is notation for a model_hash, since that is what
-     * parse reads; inline content has neither, so it must say.
+     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or
+     * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never
+     * written. Empty infers from file_path's extension, and is notation for a
+     * model_hash, since that is what parse reads; inline content has neither, so
+     * it must say.
      * 
* * string from_format = 3 [json_name = "fromFormat"]; @@ -1192,9 +1206,11 @@ public Builder clearFromFormat() { } /** *
-     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from
-     * file_path's extension, and is notation for a model_hash, since that is what
-     * parse reads; inline content has neither, so it must say.
+     * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or
+     * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never
+     * written. Empty infers from file_path's extension, and is notation for a
+     * model_hash, since that is what parse reads; inline content has neither, so
+     * it must say.
      * 
* * string from_format = 3 [json_name = "fromFormat"]; @@ -1214,7 +1230,8 @@ public Builder setFromFormatBytes( private java.lang.Object toFormat_ = ""; /** *
-     * Format to write, named as in from_format. Empty is rejected.
+     * Format to write, named as in from_format; the v1 names are refused, since
+     * a v2 model has no v1 form. Empty is rejected.
      * 
* * string to_format = 4 [json_name = "toFormat"]; @@ -1234,7 +1251,8 @@ public java.lang.String getToFormat() { } /** *
-     * Format to write, named as in from_format. Empty is rejected.
+     * Format to write, named as in from_format; the v1 names are refused, since
+     * a v2 model has no v1 form. Empty is rejected.
      * 
* * string to_format = 4 [json_name = "toFormat"]; @@ -1255,7 +1273,8 @@ public java.lang.String getToFormat() { } /** *
-     * Format to write, named as in from_format. Empty is rejected.
+     * Format to write, named as in from_format; the v1 names are refused, since
+     * a v2 model has no v1 form. Empty is rejected.
      * 
* * string to_format = 4 [json_name = "toFormat"]; @@ -1272,7 +1291,8 @@ public Builder setToFormat( } /** *
-     * Format to write, named as in from_format. Empty is rejected.
+     * Format to write, named as in from_format; the v1 names are refused, since
+     * a v2 model has no v1 form. Empty is rejected.
      * 
* * string to_format = 4 [json_name = "toFormat"]; @@ -1286,7 +1306,8 @@ public Builder clearToFormat() { } /** *
-     * Format to write, named as in from_format. Empty is rejected.
+     * Format to write, named as in from_format; the v1 names are refused, since
+     * a v2 model has no v1 form. Empty is rejected.
      * 
* * string to_format = 4 [json_name = "toFormat"]; diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertRequestOrBuilder.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertRequestOrBuilder.java index a2ad392930..65f0cf03fe 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertRequestOrBuilder.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertRequestOrBuilder.java @@ -63,9 +63,11 @@ public interface ConvertRequestOrBuilder extends /** *
-   * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from
-   * file_path's extension, and is notation for a model_hash, since that is what
-   * parse reads; inline content has neither, so it must say.
+   * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or
+   * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never
+   * written. Empty infers from file_path's extension, and is notation for a
+   * model_hash, since that is what parse reads; inline content has neither, so
+   * it must say.
    * 
* * string from_format = 3 [json_name = "fromFormat"]; @@ -74,9 +76,11 @@ public interface ConvertRequestOrBuilder extends java.lang.String getFromFormat(); /** *
-   * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from
-   * file_path's extension, and is notation for a model_hash, since that is what
-   * parse reads; inline content has neither, so it must say.
+   * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or
+   * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never
+   * written. Empty infers from file_path's extension, and is notation for a
+   * model_hash, since that is what parse reads; inline content has neither, so
+   * it must say.
    * 
* * string from_format = 3 [json_name = "fromFormat"]; @@ -87,7 +91,8 @@ public interface ConvertRequestOrBuilder extends /** *
-   * Format to write, named as in from_format. Empty is rejected.
+   * Format to write, named as in from_format; the v1 names are refused, since
+   * a v2 model has no v1 form. Empty is rejected.
    * 
* * string to_format = 4 [json_name = "toFormat"]; @@ -96,7 +101,8 @@ public interface ConvertRequestOrBuilder extends java.lang.String getToFormat(); /** *
-   * Format to write, named as in from_format. Empty is rejected.
+   * Format to write, named as in from_format; the v1 names are refused, since
+   * a v2 model has no v1 form. Empty is rejected.
    * 
* * string to_format = 4 [json_name = "toFormat"]; diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertResponse.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertResponse.java index 75d7758ff3..daa89e444b 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertResponse.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertResponse.java @@ -300,7 +300,8 @@ public org.openmbee.opensysml.proto.DiagnosticOrBuilder getDiagnosticsOrBuilder( * Set when either format is RDF, whose mapping is experimental: it covers * model structure and the behavior its bodies state, refuses what it cannot * write back, and its vocabulary may change without a compatibility path. - * Notation to notation is stable and leaves this unset. + * Also set when the source is SysML v1, whose migration is experimental in + * the same sense. Notation to notation is stable and leaves this unset. * * * bool experimental = 6 [json_name = "experimental"]; @@ -1519,7 +1520,8 @@ public org.openmbee.opensysml.proto.Diagnostic.Builder addDiagnosticsBuilder( * Set when either format is RDF, whose mapping is experimental: it covers * model structure and the behavior its bodies state, refuses what it cannot * write back, and its vocabulary may change without a compatibility path. - * Notation to notation is stable and leaves this unset. + * Also set when the source is SysML v1, whose migration is experimental in + * the same sense. Notation to notation is stable and leaves this unset. * * * bool experimental = 6 [json_name = "experimental"]; @@ -1534,7 +1536,8 @@ public boolean getExperimental() { * Set when either format is RDF, whose mapping is experimental: it covers * model structure and the behavior its bodies state, refuses what it cannot * write back, and its vocabulary may change without a compatibility path. - * Notation to notation is stable and leaves this unset. + * Also set when the source is SysML v1, whose migration is experimental in + * the same sense. Notation to notation is stable and leaves this unset. * * * bool experimental = 6 [json_name = "experimental"]; @@ -1553,7 +1556,8 @@ public Builder setExperimental(boolean value) { * Set when either format is RDF, whose mapping is experimental: it covers * model structure and the behavior its bodies state, refuses what it cannot * write back, and its vocabulary may change without a compatibility path. - * Notation to notation is stable and leaves this unset. + * Also set when the source is SysML v1, whose migration is experimental in + * the same sense. Notation to notation is stable and leaves this unset. * * * bool experimental = 6 [json_name = "experimental"]; diff --git a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertResponseOrBuilder.java b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertResponseOrBuilder.java index aeec919b8a..0d5462537d 100644 --- a/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertResponseOrBuilder.java +++ b/clients/java/opensysml-client/src/main/java/org/openmbee/opensysml/proto/ConvertResponseOrBuilder.java @@ -130,7 +130,8 @@ org.openmbee.opensysml.proto.DiagnosticOrBuilder getDiagnosticsOrBuilder( * Set when either format is RDF, whose mapping is experimental: it covers * model structure and the behavior its bodies state, refuses what it cannot * write back, and its vocabulary may change without a compatibility path. - * Notation to notation is stable and leaves this unset. + * Also set when the source is SysML v1, whose migration is experimental in + * the same sense. Notation to notation is stable and leaves this unset. * * * bool experimental = 6 [json_name = "experimental"]; diff --git a/clients/node/src/generated/sysml_pb.ts b/clients/node/src/generated/sysml_pb.ts index aa87f2f1d0..33972ef15b 100644 --- a/clients/node/src/generated/sysml_pb.ts +++ b/clients/node/src/generated/sysml_pb.ts @@ -2044,16 +2044,19 @@ export type ConvertRequest = Message<"sysml.ConvertRequest"> & { } | { case: undefined; value?: undefined }; /** - * "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from - * file_path's extension, and is notation for a model_hash, since that is what - * parse reads; inline content has neither, so it must say. + * "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or + * "mdzip" for a SysML v1 model, which is read and migrated to v2 and never + * written. Empty infers from file_path's extension, and is notation for a + * model_hash, since that is what parse reads; inline content has neither, so + * it must say. * * @generated from field: string from_format = 3; */ fromFormat: string; /** - * Format to write, named as in from_format. Empty is rejected. + * Format to write, named as in from_format; the v1 names are refused, since + * a v2 model has no v1 form. Empty is rejected. * * @generated from field: string to_format = 4; */ @@ -2120,7 +2123,8 @@ export type ConvertResponse = Message<"sysml.ConvertResponse"> & { * Set when either format is RDF, whose mapping is experimental: it covers * model structure and the behavior its bodies state, refuses what it cannot * write back, and its vocabulary may change without a compatibility path. - * Notation to notation is stable and leaves this unset. + * Also set when the source is SysML v1, whose migration is experimental in + * the same sense. Notation to notation is stable and leaves this unset. * * @generated from field: bool experimental = 6; */ diff --git a/clients/python/opensysml/__init__.py b/clients/python/opensysml/__init__.py index d2b41ba907..d8cb65d6cf 100644 --- a/clients/python/opensysml/__init__.py +++ b/clients/python/opensysml/__init__.py @@ -239,9 +239,10 @@ def convert(to_format, file_path=None, content=None, model_hash=None, content (str, optional): Source carried inline model_hash (str, optional): Hash of a loaded model, whose parsed source is converted - from_format (str, optional): Format to read the source as; inferred from - file_path's extension when omitted, notation for a model_hash, and - required for inline content + from_format (str, optional): Format to read the source as, one of the + to_format names or 'xmi', 'uml' or 'mdzip' for a SysML v1 model to + migrate; inferred from file_path's extension when omitted, notation + for a model_hash, and required for inline content tolerate_syntax_errors (bool): Write notation back out even when the parser could not read all of it host (str): Service hostname, or a ``host:port`` address @@ -252,7 +253,9 @@ def convert(to_format, file_path=None, content=None, model_hash=None, Warns: ExperimentalFeatureWarning: If either format is RDF, whose mapping is - experimental — see ``docs/reference/rdf-mapping.md`` + experimental (see ``docs/reference/rdf-mapping.md``), or the source + is SysML v1, whose migration is experimental too (see + ``docs/reference/sysml-v1-migration.md``) Example: >>> import opensysml diff --git a/clients/python/opensysml/connection.py b/clients/python/opensysml/connection.py index f9c29f5e2b..36f8f2c008 100644 --- a/clients/python/opensysml/connection.py +++ b/clients/python/opensysml/connection.py @@ -842,9 +842,11 @@ def convert(self, to_format, file_path=None, content=None, model_hash=None, content (str, optional): Source carried inline model_hash (str, optional): Hash of a loaded model, whose parsed source is converted - from_format (str, optional): Format to read the source as; inferred - from file_path's extension when omitted, notation for a - model_hash, and required for inline content + from_format (str, optional): Format to read the source as, one of + the to_format names or 'xmi', 'uml' or 'mdzip' for a SysML v1 + model to migrate; inferred from file_path's extension when + omitted, notation for a model_hash, and required for inline + content tolerate_syntax_errors (bool): Write notation back out even when the parser could not read all of it, reporting its syntax errors as the result's diagnostics. Notation to notation only: every other @@ -857,7 +859,9 @@ def convert(self, to_format, file_path=None, content=None, model_hash=None, Warns: ExperimentalFeatureWarning: If either format is RDF, whose mapping is - experimental — see ``docs/reference/rdf-mapping.md`` + experimental (see ``docs/reference/rdf-mapping.md``), or the + source is SysML v1, whose migration is experimental too (see + ``docs/reference/sysml-v1-migration.md``) Raises: ValueError: If other than one of file_path, content and model_hash diff --git a/clients/rust/conformance/sysml.descriptor.binpb b/clients/rust/conformance/sysml.descriptor.binpb index bf6221c3b9..3ffc5b24a4 100644 Binary files a/clients/rust/conformance/sysml.descriptor.binpb and b/clients/rust/conformance/sysml.descriptor.binpb differ diff --git a/clients/rust/opensysml/src/proto/sysml/sysml.rs b/clients/rust/opensysml/src/proto/sysml/sysml.rs index 669829c0d1..c57d922188 100644 --- a/clients/rust/opensysml/src/proto/sysml/sysml.rs +++ b/clients/rust/opensysml/src/proto/sysml/sysml.rs @@ -880,12 +880,15 @@ pub struct ExecuteStateResponse { /// change the answer; a file_path is read afresh and content is carried inline. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ConvertRequest { - /// "sysml", "kerml", "text", "ttl", "turtle" or "rdf". Empty infers from - /// file_path's extension, and is notation for a model_hash, since that is what - /// parse reads; inline content has neither, so it must say. + /// "sysml", "kerml", "text", "ttl", "turtle" or "rdf", or "xmi", "uml" or + /// "mdzip" for a SysML v1 model, which is read and migrated to v2 and never + /// written. Empty infers from file_path's extension, and is notation for a + /// model_hash, since that is what parse reads; inline content has neither, so + /// it must say. #[prost(string, tag="3")] pub from_format: ::prost::alloc::string::String, - /// Format to write, named as in from_format. Empty is rejected. + /// Format to write, named as in from_format; the v1 names are refused, since + /// a v2 model has no v1 form. Empty is rejected. #[prost(string, tag="4")] pub to_format: ::prost::alloc::string::String, /// Write notation back out even when the parser could not read all of it, @@ -930,7 +933,8 @@ pub struct ConvertResponse { /// Set when either format is RDF, whose mapping is experimental: it covers /// model structure and the behavior its bodies state, refuses what it cannot /// write back, and its vocabulary may change without a compatibility path. - /// Notation to notation is stable and leaves this unset. + /// Also set when the source is SysML v1, whose migration is experimental in + /// the same sense. Notation to notation is stable and leaves this unset. #[prost(bool, tag="6")] pub experimental: bool, /// What is experimental about the conversion, in the wording every surface diff --git a/docs/guide/07-saving-and-rdf.md b/docs/guide/07-saving-and-rdf.md index dc65838b66..aeb15e360c 100644 --- a/docs/guide/07-saving-and-rdf.md +++ b/docs/guide/07-saving-and-rdf.md @@ -89,8 +89,9 @@ sysml input.txt -convert ttl -from sysml `-convert` and `-from` accept `sysml`, `kerml`, `text`, `ttl`, `turtle` and `rdf`; `-from` also accepts `xmi` (or `uml`, `mdzip`) for a SysML v1 model exported as OMG XMI, an Eclipse `.uml` file or a zip archive holding the XMI (a MagicDraw/Cameo `.mdzip` project), which is migrated -to v2 on the way in — an experimental path, and every run that takes it says so on stderr; see -[SysML v1 migration](../reference/sysml-v1-migration.md). The output path +to v2 on the way in — an experimental path, and every run that takes it says so on stderr; +[chapter 11](11-migrating-from-sysml-v1.md) walks one through, and +[SysML v1 migration](../reference/sysml-v1-migration.md) states the mapping. The output path plays no part in choosing the format, so a destination without an extension, such as `-o /dev/null` or a FIFO, needs no extra flags. diff --git a/docs/guide/09-clients.md b/docs/guide/09-clients.md index bdd464c057..84c662e8fc 100644 --- a/docs/guide/09-clients.md +++ b/docs/guide/09-clients.md @@ -1002,6 +1002,22 @@ text, and `write(path)` saves it. Formats are named `sysml`, `kerml`, `text`, `t `rdf`. A file path's format is inferred from its extension; inline `content` has no extension, so it needs `from_format`. +`convert` also reads a **SysML v1** model and migrates it, when the source is UML XMI, an Eclipse +UML2 `.uml` file or a `.mdzip` archive: `from_format` is `xmi`, `uml` or `mdzip`, inferred from +those extensions, and is an input only — asking to write it raises `InvalidRequestError`, since a +v2 model has no v1 form. Migration is +[experimental](../reference/sysml-v1-migration.md#status-experimental) and warns as the RDF +direction does. The service does not return the migration report; run +`sysml Model.xmi -convert sysml -migration-report Model.report.txt` for the element-by-element +account, as [chapter 11](11-migrating-from-sysml-v1.md) walks through. + +```python +migrated = opensysml.convert("sysml", file_path="Vehicle.mdzip") # ExperimentalFeatureWarning +migrated.from_format, migrated.to_format # ('xmi', 'sysml') +migrated.write("Vehicle.sysml") +opensysml.convert("ttl", content=xmi_text, from_format="xmi") # straight to RDF +``` + A `Model` writes out the source the service parsed, identified by `model.hash`, so editing the file between `load` and `save` does not change what is written: the model saved is the model you inspected. `convert(file_path=…)` is the alternative, and reads the file as it is now. The @@ -1025,7 +1041,8 @@ What each direction preserves: Conversion is capability-negotiated: against a service that does not report the `convert` capability, these calls raise `MissingCapabilityError` naming the required upgrade rather than failing on an unimplemented method. For a service that does not report the RDF mapping's status, -the status is worked out from the formats it reports, so an RDF conversion warns either way. +the status is worked out from the formats it reports, so an RDF conversion or a v1 migration +warns either way. Suppress the warning with `warnings.simplefilter("ignore", opensysml.ExperimentalFeatureWarning)`; no stable feature uses that warning class. diff --git a/docs/guide/11-migrating-from-sysml-v1.md b/docs/guide/11-migrating-from-sysml-v1.md new file mode 100644 index 0000000000..0016c3ccd7 --- /dev/null +++ b/docs/guide/11-migrating-from-sysml-v1.md @@ -0,0 +1,207 @@ +# 11. Migrating a SysML v1 model + +A SysML v1 model exported from its tool as UML XMI, an Eclipse UML2 `.uml` file or a +MagicDraw/Cameo `.mdzip` archive can be read by `sysml -convert` and written as SysML v2 notation +or RDF Turtle. This chapter walks one export through the migration: running it, reading the +report it produces, finishing by hand what the mapping leaves behind, and checking the result as +a v2 model. Every mapping rule, and the format of the report, is in +[reference/sysml-v1-migration.md](../reference/sysml-v1-migration.md). + +Migration is **experimental**: the mapping covers structure, ports and connectors, requirements, +constraints, instances and allocations; it does not yet migrate behaviors, operations or units; +and what it writes for a v1 element may change between releases without a compatibility path. +Every run says so on stderr, so a script sees the status without reading this page. It is also +**one way**: a v2 model has no v1 form, so `-convert xmi` is refused, and the migrated notation is +the place to keep working — not a copy to be re-migrated after editing the v1 model. + +## The example + +The model migrated below is the vehicle fixture the migration's own tests use, +[`internal/core/migrate/testdata/xmi/vehicle.xmi`](../../internal/core/migrate/testdata/xmi/vehicle.xmi): +a small vehicle design of blocks, value types, ports and connectors, a constraint block, an +instance, a requirements package with satisfy, verify and derive relationships, and — so that the +report has something to say — an activity, a state machine, an operation, a unit and a quantity +kind. Copy it next to you as `Vehicle.xmi` to follow along; your own tool's export works the +same way, whatever its extension: + +```bash +sysml Vehicle.xmi -convert sysml -o Vehicle.sysml # OMG XMI 2.5.1 with the SysML profile +sysml Vehicle.uml -convert sysml -o Vehicle.sysml # Eclipse UML2 / Papyrus +sysml Vehicle.mdzip -convert sysml -o Vehicle.sysml # MagicDraw / Cameo project archive +sysml export.xml -convert sysml -o Vehicle.sysml -from xmi # an extension that does not say +``` + +The input format is inferred from the extension and named with `-from` when the extension does +not say. Diagrams and the exporting tool's private state are skipped; the model is what is read. + +## Running the migration + +Ask for the report alongside the notation. Without `-migration-report` only its one-line summary +is printed: + +```console +$ sysml Vehicle.xmi -convert sysml -o Vehicle.sysml -migration-report Vehicle.report.txt +note: SysML v1 migration is experimental: the mapping covers structure, ports and connectors, requirements, constraints, instances and allocations, reports every element it approximates or leaves behind, and what it writes for a v1 element may change without a compatibility path; see docs/reference/sysml-v1-migration.md § Status +wrote Vehicle.report.txt (migration report: migrated 89 element(s): 68 mapped, 13 approximated, 8 unmapped (3 skipped as profile or library content)) +wrote Vehicle.sysml (sysml, 5913 bytes) +``` + +The summary is the first thing to read: 89 v1 elements, of which 68 have a direct v2 form, 13 +were written as the nearest v2 construct, and 8 have none. The 3 skipped are the SysML profile +application, the profile itself and a diagram — not model content, so they count against nothing. +The command exits 0 when the notation was written, whatever the report says; it exits non-zero +and writes nothing when the input cannot be read as XMI at all, or holds no model. + +A report named `.json` is written as JSON with the same entries, for a script that wants to gate +on the verdicts rather than read them: + +```bash +sysml Vehicle.xmi -convert sysml -o Vehicle.sysml -migration-report Vehicle.report.json +``` + +`-convert ttl` writes the migrated model as RDF Turtle in one step, through the same mapping and +then [the RDF mapping](07-saving-and-rdf.md); the report describes the migration either way. + +## Reading the report + +The report accounts for every element of the v1 model under one of four verdicts, the ones that +need attention first: + +```text +# SysML v1 to v2 migration report: Vehicle.xmi +# exported by Example UML Tool +# migrated 89 element(s): 68 mapped, 13 approximated, 8 unmapped (3 skipped as profile or library content) + +## unmapped (8) +«Verify» Abstraction Requirements:: _dep_verify_block (a verify whose client is not a test case has no v2 form; applied stereotypes «Verify») +«Refine» Abstraction Requirements:: _dep_refine (its end Vehicle Design::Drive is not migrated; applied stereotypes «Refine») +«Allocate» Abstraction Requirements:: _dep_allocate (its end Vehicle Design::Drive is not migrated; applied stereotypes «Allocate») +Activity Vehicle Design::Drive _act_drive (behaviors are not migrated yet) +«Unit» InstanceSpecification Vehicle Design::Value Types::kilogram _unit_kg (units and quantity kinds are not migrated; use the SI and ISQ libraries; applied stereotypes «Unit» (quantityKind = Vehicle Design::Value Types::mass; symbol = kg)) +«QuantityKind» InstanceSpecification Vehicle Design::Value Types::mass _qk_mass (units and quantity kinds are not migrated; use the SI and ISQ libraries; applied stereotypes «QuantityKind») +StateMachine Vehicle Design::Vehicle::Vehicle States _sm_vehicle (behaviors are not migrated yet) +Operation Vehicle Design::Vehicle::start _op_start (operations and receptions are not migrated; v2 has no operation) + +## approximated (13) +«Trace» Abstraction Requirements:: _dep_trace (a trace is written as a plain dependency) +«TestCase» Activity Requirements::Mass Test _tc_mass -> Requirements::'Mass Test' (the test case's behavior is not migrated; only its verified requirements are) +Actor Vehicle Design::Driver _actor_driver -> 'Vehicle Design'::Driver (a UML actor is written as a part def) +«FlowPort» Port Vehicle Design::Engine::speedIn _port_speedIn -> 'Vehicle Design'::Engine::speedIn (a port typed by a DataType is written as a port holding one directed attribute) +… +``` + +Each line is the v1 element's kind with its applied stereotypes, its qualified name in the v1 +model, its `xmi:id`, the v2 element it became (after `->`) when one was written, and the reason +for the verdict in parentheses. The columns are tab-separated, so `cut` and `awk` read them. + +- **mapped** — a direct v2 form. Nothing to do. +- **approximated** — written as the nearest v2 construct, and the note says what was lost. Some + of these are simply how v2 spells the idea (a UML actor is a `part def`; a flow port typed by a + data type is a port holding one directed attribute; a `«Trace»` is a `dependency`). Others + record a v1 tag that v2 has no home for (`isEncapsulated`, a value type's `unit` and + `quantityKind`), or an opaque expression copied verbatim in a language other than SysML, which + the v2 model will not evaluate. +- **unmapped** — nothing was written for the element. The notation holds a comment where it + would have gone, so the loss is visible in the file, not only in the report. The three + `Abstraction`s are unmapped *because* their other end, the activity `Drive`, is: a relationship + cannot be written to an element that does not exist. +- **skipped** — profile, library and tool-private content. Never model content. + +The migration keeps the v1 names, quoting them where v2 requires it, and keeps a requirement's +`id` as its short name, so `Mass Requirement` with id `R1` is `requirement def 'Mass +Requirement'` and a v1 qualified name can be searched for in the v2 file as it stands. + +## Finishing by hand + +Open `Vehicle.sysml`. Alongside every element the report calls unmapped or approximated-with-loss +is a comment saying so, at the place in the v2 model where the element belongs: + +```sysml +package 'Value Types' { + attribute def Mass :> ScalarValues::Real { + /* «ValueType» tags with no v2 form: quantityKind = Vehicle Design::Value Types::mass; unit = Vehicle Design::Value Types::kilogram */ + } + /* not migrated: «Unit» InstanceSpecification 'kilogram' — units and quantity kinds are not migrated; use the SI and ISQ libraries; applied stereotypes «Unit» (quantityKind = Vehicle Design::Value Types::mass; symbol = kg) */ + … +} +part def Vehicle :> System { + … + /* not migrated: Operation 'start' — operations and receptions are not migrated; v2 has no operation */ + /* not migrated: StateMachine 'Vehicle States' — behaviors are not migrated yet */ + … +} +``` + +Each comment is a to-do, and its note says which way to go: + +- **Units and quantity kinds.** v2 ships them: import `SI` and `ISQ` and type the attribute by + the library's quantity, `attribute def Mass :> ISQ::MassValue;` with `mass = 1200 [SI::kg]` as + its value, in place of the v1 value type that carried a unit tag. The mapping reports rather + than guesses here, since the v1 model's own `kilogram` may or may not be the SI one. +- **Behaviors.** The activity `Drive` and the state machine `Vehicle States` are written in the + v2 notation of [chapter 6](06-behavior.md) — `action def`, `state def`, transitions with + triggers and guards — and the `«Allocate»` and `«Refine»` relationships that pointed at `Drive` + are restored once it exists, as an `allocate` and as a `dependency` carrying + `@ModelingMetadata::Refinement`, the forms the mapping writes when both ends are present. +- **Operations.** v2 has no operation; a v1 operation on a block becomes a `perform action` or + an action usage owned by the part, its parameters the action's `in` and `out`. +- **Opaque expressions.** A v1 constraint or default is copied verbatim when its text parses as + a v2 expression, and written as a comment otherwise. `constraint 'positive mass' { mass > 0 }` + was copied from an opaque expression whose declared language is `English`; it reads the same + in v2, but a copied expression means what v2 says it means, not what the original language + did, so the report's `opaque expression copied verbatim (language …)` notes are the + constraints to read, and the commented ones are the constraints to rewrite. +- **Tags with no v2 form.** `isEncapsulated`, a custom stereotype's tags, a value type's + `unit`: keep the comment as documentation, or express the intent in v2 terms (a `metadata def` + for a stereotype that carries meaning the model relies on). + +Where the v1 model has a relationship to an element outside the exported file — a used +project, a resource referenced by `href` — that end is an external proxy, and the relationship +is reported unmapped rather than written to nothing. Migrate the other model too, then reconnect +them in v2. + +## Checking the result + +The migrated notation is an ordinary v2 model from here on: every check in +[chapter 3](03-command-line.md) applies, and the file parses and analyses under the same passes +as a hand-written one. This model's `Vehicle` and its instance `myCar` are intact — the value +`myCar` gives its mass is what the v1 slot held: + +```console +$ sysml Vehicle.sysml -eval "'Vehicle Design'::myCar::mass" +Vehicle.sysml:85:9: warning: End feature must have multiplicity 1: an end relates exactly one thing per link; write `[1]` or take it from a feature the end subsets or redefines + end driver : Driver[0..1]; + ^~~~~~~~~~~~~~~~~~~~~~~~~~ +✓ package 'Vehicle Design' +✓ package Requirements +✓ package 'Empty Package' + = 1350.5 +``` + +Findings on the migrated file are read as on any model, and point at what to revise in the v2 +notation — here the association `Drives`, whose v1 end multiplicity `0..1` v2 writes on the end +rather than after its type: `end [0..1] driver : Driver;`. From this point `%save` in the REPL, +`-convert ttl`, the LSP and the clients all take the file as they take any other; nothing +remembers that it was migrated. + +## Over gRPC and from a program + +The same migration is the service's `Convert` with `from_format` of `xmi`, `uml` or `mdzip` — +inferred from `file_path`'s extension when omitted — and a `to_format` of notation or Turtle. +The response marks it `experimental` with the notice above, which the Python client raises as an +`ExperimentalFeatureWarning` and the Go client reports on the `Conversion`. The report is not on +the wire: a program that needs the element-by-element account runs the command. How each client +exposes it is in [chapter 9](09-clients.md#writing-a-model-back-out), and the wire fields in +[reference/wire-contract.md](../reference/wire-contract.md#conversion-convert). + +```python +migrated = opensysml.convert("sysml", file_path="Vehicle.mdzip") +migrated.write("Vehicle.sysml") +``` + +## Where the mapping stops + +What the mapping does not do is deliberate rather than an oversight, and is tracked on the +[roadmap](../project/roadmap.md): behaviors, operations and receptions, units and quantity kinds, +and the report over gRPC. Until then, the report is the contract — a v1 element is either in the +v2 model, or named in the report with the reason it is not, never silently dropped. diff --git a/docs/guide/README.md b/docs/guide/README.md index 28cf81f527..18f1dc1e86 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -12,6 +12,7 @@ Read the chapters in order the first time through; each one builds on the ones b 8. [Editors](08-editors.md) — `sysml-lsp` and the VS Code extension 9. [From your own program](09-clients.md) — the Go, Python, Node, Java and Rust clients 10. [Troubleshooting](10-troubleshooting.md) — diagnosing a run that stops early +11. [Migrating a SysML v1 model](11-migrating-from-sysml-v1.md) — XMI, `.uml` or `.mdzip` in, v2 notation and a report out Chapter 9 does one task in all five clients side by side — Go, Python, Node/TypeScript, Java and Rust, in tabs — and then has a section per client for what only that one has. diff --git a/docs/project/roadmap.md b/docs/project/roadmap.md index 7f2d405913..98ede18a66 100644 --- a/docs/project/roadmap.md +++ b/docs/project/roadmap.md @@ -13,7 +13,7 @@ its first step, under "Proposed"), and the state-executor fixes the PSSM referee Read `AGENTS.md` first; it governs everything below. > **Labels.** This is an engineering record. The RDF items keep the `D` numbers (`D1`, `D2`, -> `D3.4`, `D7`, `D8`, `D9`, `D10`, `D11`, `D12`) that other records, the known-violations inventory +> `D3.4`, `D7`, `D8`, `D9`, `D10`, `D11`, `D12`, `D13`) that other records, the known-violations inventory > and the ontology package's README cross-reference; `L` names the library items, `N` the native > compilation track, `R` the release follow-through, `W` the diagram output formats a view > rendering is written in, `F` the executor defects the conformance gate carried as @@ -958,6 +958,60 @@ What landed: Extends D3's identity work; D11's `api-json` payloads are the first surface where a foreign reader compares our library ids to its own. +## D13 — SysML v1 migration: the rest of the model, and the report on the wire + +`-convert` reads a SysML v1 model — OMG UML XMI 2.5.1 with the SysML profile, an Eclipse UML2 +`.uml` file, a `.mdzip` archive — and writes it as v2 notation or Turtle, with an +element-by-element report; see [the migration reference](../reference/sysml-v1-migration.md) and +[guide chapter 11](../guide/11-migrating-from-sysml-v1.md). It ships **experimental**, and the +reference is precise about where it stops: the mapping covers structure, ports and connectors, +requirements, constraints, instances and allocations, and reports the rest as unmapped rather +than guessing. Over the TMT model roughly half the elements map or are approximated, and the +unmapped half is dominated by the items below. Promoting the migration to stable is closing them +and then measuring, not a documentation change. Each is its own pull request, in this order: + +1. **Behaviors.** Activity, StateMachine, Interaction and OpaqueBehavior are comment + placeholders today. Activities become `action def`s with their nodes as owned actions, object + flows as `flow`s and control flows as `succession`s — the constructs + [guide chapter 6](../guide/06-behavior.md) executes — and state machines become `state def`s + with regions, transitions, triggers and guards; interactions become the message-and-succession + form the executor already runs. The report's `behaviors are not migrated yet` verdicts move to + `mapped`, and the relationships that were unmapped because their end was a behavior (the + `«Allocate»` and `«Refine»` to an activity) follow them. Gate: the fixture's activities and + state machines run under `%run` with the trace the v1 model implies. +2. **Operations and receptions.** v2 has no operation; the nearest form is an action usage owned + by the part with the operation's parameters as its `in`/`out`, and a reception as an + `accept` of the signal's attribute def. The mapping picks one form and says so in the + reference, so a migrated operation is `approximated` with a note, not `unmapped`. +3. **Units and quantity kinds.** `«Unit»` and `«QuantityKind»` instance specifications, and + the `unit`/`quantityKind` tags of a `«ValueType»`, are reported and commented; the value type + is written over `ScalarValues::Real`. Map a value type whose unit resolves to a known SI or + ISQ symbol onto `ISQ` quantity values with `SI` units (`attribute def Mass :> ISQ::MassValue`, + defaults as `1200 [SI::kg]`), and keep the comment for a unit that does not resolve. The + symbol table is Track L's L5, so this item follows it. +4. **The report over gRPC and in the clients.** The migration report exists only where the CLI + writes it; `Convert` answers the migrated content and the experimental notice, nothing more. + Add the report to `ConvertResponse` as a repeated entry message with the same fields as the + JSON form (id, kind, name, verdict, target, note), populated only when the source is v1, and + surface it as a `Conversion` attribute in the Python and Go clients, so a program can gate on + verdicts as a script gates on the `.json` report. Node, Java and Rust read the field from the + generated code without a wrapper, as they do the notice. +5. **Stable identity for a re-migration.** The v1 `xmi:id` is kept only in the report; the + notation carries no ids, so migrating a model twice produces two unrelated v2 models, and an + edit made in v2 cannot be matched against a later v1 export. Once + [element identity annotations](element-identity-annotations.md) land, write the `xmi:id` as + the element's identity, so a re-migration can be diffed against the edited model rather than + replacing it. Gated on D3's identity work. +6. **Stable, then measured.** With 1–4 landed, re-run the TMT measurement the reference quotes + and pin the per-verdict counts as the corpus ratchet pins round trips; the mapping's forms + become subject to the compatibility path that lifting the experimental notice promises, and + the notice is removed from every surface at once (`export.IsExperimental`, the Python warning, + the Go `Conversion`). + +Independent of D1, D2 and D7, which concern the RDF form the migrated model may be written into, +not the migration. Item 4 touches the wire contract and is a Track I change in the same pull +request. + --- # Track F — the executor defects the conformance gate carried (closed) @@ -2486,7 +2540,8 @@ carried more than that list. By track, with the pull requests the tracks cite: - **Track W** — landed: W1 (`dot`), W2 (`plantuml`) and W3's plumbing for both. - **Track E** — E9 and E10 landed as conformance findings; E8's refusal landed (#229), the item itself is open. -- **Track D** — D12 (the standard library's normative element ids) is done. +- **Track D** — D12 (the standard library's normative element ids) is done; D13 (the rest of + the SysML v1 migration, and its report on the wire) is proposed. - **Release follow-through** — R4's Windows installer is published by `v0.7.0` and `v0.8.0` alike; the release procedure runs git-flow (#151); `opensysml` 0.5.0 is on PyPI; the test-suite figures are generated and gated (#291, on `develop` after the tag). @@ -2587,7 +2642,9 @@ that exercise them. The decision is the release checklist's, recorded there. previous repository) have to be re-proposed against this repository before **D8**'s profile, which only becomes conformant behind D1 and D2; **D12** (the standard library's normative element ids) is done; **D11** (the API element form) after D1 and D2, and before D9.2 if the - branch read is to offer it; **D10** (write-through from a view-only project) after D9.1 and + branch read is to offer it; **D13** (the SysML v1 migration's behaviors, operations, units, + report over gRPC and identity) is independent of the RDF items and starts with behaviors, its + units item after L5 and its identity item after D3; **D10** (write-through from a view-only project) after D9.1 and D9.2, which it reads and writes through. - **Track F.** Closed. F1 and F2 landed together (#116) as the token-per-succession model, F3 (#120) as the per-traversal merge on top of it; `known_failures.txt` has no line left to delete. diff --git a/docs/reference/api.md b/docs/reference/api.md index d526914fb8..13c2d125af 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -81,6 +81,26 @@ for _, doc := range result.Documents { } ``` +`Convert`, `ConvertFile` and `ConvertSource` write a model out in another `Format`: `FormatSysML` +(aliases `FormatKerML`, `FormatText`) or `FormatTTL` (`FormatTurtle`, `FormatRDF`). `ConvertFile` +infers the source format from the extension unless `WithFromFormat` names it, and `ConvertSource` +requires it. A SysML v1 model — UML XMI, an Eclipse UML2 `.uml` file or a `.mdzip` archive — is +`FormatXMI`, an input only: `ConvertFile(ctx, "Model.xmi", FormatSysML)` migrates it to v2 +notation, `FormatTTL` to RDF, and asking to write `FormatXMI` is `CodeInvalidArgument`. The +`Conversion` reports the canonical `From` and `To`, and `Experimental` with its +`ExperimentalNotice` when either side is RDF or the source is v1, both of which are experimental +mappings. The service does not return the migration report the `sysml` command writes with +`-migration-report`; what the migration covers is in +[sysml-v1-migration.md](sysml-v1-migration.md). + +```go +conversion, err := client.ConvertFile(ctx, "Vehicle.mdzip", opensysml.FormatSysML) +if conversion.Experimental { + log.Println(conversion.ExperimentalNotice) +} +os.WriteFile("Vehicle.sysml", []byte(conversion.Content), 0o644) +``` + Its errors, ownership rules, capability negotiation and v1 boundary are in [client/opensysml/README.md](../../client/opensysml/README.md), and the other client languages are on [client libraries](clients.md). A program with no client library that posts JSON to the service diff --git a/docs/reference/python-api.md b/docs/reference/python-api.md index e724d9023f..21816c7061 100644 --- a/docs/reference/python-api.md +++ b/docs/reference/python-api.md @@ -283,8 +283,9 @@ make python-proto - `model.py` — a parsed model: root symbol and diagnostics - `symbol.py` — lazy symbol proxy, fetches children on demand - `instance.py` — instantiated object and its feature values -- `conversion.py` — a written model, its formats, extension inference, and the - `ExperimentalFeatureWarning` an RDF conversion raises +- `conversion.py` — a written model, its formats, extension inference (`.xmi`, `.uml` and + `.mdzip` among them, for a SysML v1 model migrated on the way in), and the + `ExperimentalFeatureWarning` an RDF conversion or a v1 migration raises - `query.py` — the standard's Query payload, translated and its answers - `document.py` — native document queries: typed bindings, typed rows, and `model.render_document`'s Markdown diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index 51e79941db..6667a4b69d 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -1765,6 +1765,55 @@ $ … /VerifyConstraint -d '{"modelHash":"b4e0…ded9","symbolId":"Demo::Vehicle The Python client reads them as `Verdict.engine`, `Verdict.strength` and `Verdict.bounds` and lists engines with `Connection.list_engines()`. +## Conversion: `Convert` + +`Convert` writes a model out in another representation, and needs the `convert` capability. The +request names its source in a `oneof`: a `filePath` the service reads afresh, `content` carried +inline, or a `modelHash` whose parsed source is converted. `toFormat` is required and is one of +`sysml`, `kerml`, `text` (SysML v2 notation) or `ttl`, `turtle`, `rdf` (RDF in Turtle). +`fromFormat` takes the same names, plus `xmi`, `uml` or `mdzip` for a SysML v1 model — UML XMI +2.5.1 with the SysML profile applied, an Eclipse UML2 `.uml` file, or a `.mdzip` archive — which +is read and **migrated** to v2 on the way out. Omitted, `fromFormat` is inferred from `filePath`'s +extension (`.sysml`, `.kerml`, `.ttl`, `.turtle`, `.xmi`, `.uml`, `.mdzip`), is notation for a +`modelHash`, and is `invalid_argument` for inline `content`, which has no extension. Inline +content is a proto `string`, so it carries XMI or `.uml` text; a `.mdzip` archive is binary and is +named by `filePath`. + +```console +$ … /Convert -d '{"filePath":"Vehicle.xmi","toFormat":"sysml"}' +{ + "content": "doc /* Author: demo team\n * Created: 2026-09-05\n */\npackage 'Vehicle Design' {\n doc /* Structural model of the demo v…", + "fromFormat": "xmi", + "toFormat": "sysml", + "experimental": true, + "experimentalNotice": "SysML v1 migration is experimental: the mapping covers structure, ports and connectors, requirements, constraints, instances and allocations, reports every element it approximates or leaves behind, and what it writes for a v1 element may change without a compatibility path; see docs/reference/sysml-v1-migration.md § Status" +} +``` + +`fromFormat` and `toFormat` come back **canonical** — `sysml`, `ttl` or `xmi` whichever alias +was sent — so a client that let the format be inferred learns what it was read as. +`experimental` is set, and `experimentalNotice` says why, when either format is RDF or the source +is SysML v1; notation to notation leaves both unset. It is set on a refusal too, so read it before +`error`. The Python client raises `ExperimentalFeatureWarning` from it. The migration report the +`sysml` command writes with `-migration-report` is **not** on the wire: a client that needs the +element-by-element account runs the command. What the migration maps, approximates and leaves +behind is in [sysml-v1-migration.md](sysml-v1-migration.md). + +A conversion that could not be done is HTTP 200 with `error` set and `content` absent; its +`diagnostics` explain a syntax error in notation input, with spans. Malformed XMI is reported in +`error` alone: + +```text +{"fromFormat":"xmi","toFormat":"sysml","error":": the XMI document holds no model: expected a uml:Model or uml:Package under the xmi:XMI root","experimental":true,"experimentalNotice":"SysML v1 migration is experimental: …"} +``` + +A request the service will not attempt is a Connect error instead: `toFormat` naming a v1 format +is `invalid_argument` with `cannot write xmi: SysML v1 XMI is read and migrated, never written; +convert to sysml or ttl`, since a v2 model has no v1 form; an unknown format name and a missing +`fromFormat` for inline content are `invalid_argument` too; an unreadable `filePath` is +`not_found` with `file not found:`, and a stale `modelHash` is `not_found` as described under +[the model hash](#how-long-a-hash-is-valid). + ## Queries Two query surfaces exist and answer differently shaped tables. Their semantics — what may be diff --git a/mkdocs.yml b/mkdocs.yml index 2f5277f1a8..afb11de29f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -169,6 +169,7 @@ nav: - guide/08-editors.md - guide/09-clients.md - guide/10-troubleshooting.md + - guide/11-migrating-from-sysml-v1.md - Document generation: - manual/README.md - Introduction and concepts: manual/introduction.md