From 5c97cbd3c4f579e93000f8ac3f2815eb86b77c6c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:37:17 +0000 Subject: [PATCH 1/5] wip: untyped property kind Co-Authored-By: jason.han --- internal/translate/migrate/migrate.go | 39 ++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index 6824338ed3..973f815c93 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -2209,7 +2209,7 @@ func (m *migration) featureKeyword(p *sysmlv1.Element, owner category) (keyword, return "port", "", "" } if t == nil { - return "ref", "", "the untyped property is written as a reference usage" + return m.untypedKeyword(p) } if owner == catUseCaseDef && t.Type == "Actor" && m.written(t) { return "actor", "", "" @@ -2253,6 +2253,18 @@ func (m *migration) featureKeyword(p *sysmlv1.Element, owner category) (keyword, return kw, "", note } +// untypedKeyword is the usage an untyped property is written as: the kind its +// tool's marker gives it, else a part when composite and an attribute otherwise. +func (m *migration) untypedKeyword(p *sysmlv1.Element) (keyword, prefix, note string) { + if kind := m.markedPropertyKind(p); kind != "" { + return kind, "", "" + } + if p.Attrs["aggregation"] == "composite" { + return "part", "", "the untyped composite property is written as a part" + } + return "attribute", "", "the untyped property is written as an attribute" +} + // typeKeyword is the usage keyword a property takes from its type alone, // before its owner and aggregation weigh in. func (m *migration) typeKeyword(t *sysmlv1.Element) (keyword, note string) { @@ -4181,9 +4193,8 @@ var propertyKindMarkers = map[string]string{ // isPropertyKindMarker recognises MagicDraw's marker of a property's kind, which // the usage's keyword already says; a same-named stereotype from elsewhere is kept. func (m *migration) isPropertyKindMarker(e *sysmlv1.Element, s *sysmlv1.Stereotype) bool { - kind, ok := propertyKindMarkers[s.Name] - if !ok || len(s.Tags) > 0 || e.Type != "Property" || e.Parent == nil || - !isMagicDrawCustomization(s.Namespace) { + kind := propertyKindMarker(e, s) + if kind == "" || e.Parent == nil { return false } owner, _ := m.classify(e.Parent) @@ -4194,6 +4205,26 @@ func (m *migration) isPropertyKindMarker(e *sysmlv1.Element, s *sysmlv1.Stereoty return usageKind(kw, prefix) == kind } +// propertyKindMarker is the kind of usage MagicDraw's marker s says property e +// is, or empty when s is not such a marker. +func propertyKindMarker(e *sysmlv1.Element, s *sysmlv1.Stereotype) string { + kind, ok := propertyKindMarkers[s.Name] + if !ok || len(s.Tags) > 0 || e.Type != "Property" || !isMagicDrawCustomization(s.Namespace) { + return "" + } + return kind +} + +// markedPropertyKind is the kind MagicDraw's marker on property e gives it, if any. +func (m *migration) markedPropertyKind(e *sysmlv1.Element) string { + for _, s := range e.Stereotypes { + if kind := propertyKindMarker(e, s); kind != "" { + return kind + } + } + return "" +} + // usageKind is the kind of property a usage keyword and its `ref ` prefix express. func usageKind(kw, prefix string) string { switch { From 4242ff7a490692ce01db1efca8300a771c9412ba Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:14:45 +0000 Subject: [PATCH 2/5] fix(migrate): an untyped property follows its tool's kind marker; unmarked, it is an attribute, or a part when composite Co-Authored-By: jason.han --- .../unreleased/untyped-property-kind.fixed.md | 1 + ...sml-v1-transformation-census-baseline.json | 12 +++--- .../project/sysml-v1-transformation-census.md | 41 ++++++++++--------- docs/reference/sysml-v1-migration.md | 4 +- .../migrate/behavior_members_internal_test.go | 4 +- .../migrate/diagrams_internal_test.go | 6 +-- internal/translate/migrate/migrate.go | 6 ++- tests/migrate/properties_common_test.go | 4 +- tests/migrate/relations_test.go | 34 ++++++++++----- .../testdata/xmi/collectors.golden.report.txt | 2 +- .../testdata/xmi/collectors.golden.sysml | 2 +- .../xmi/property_markers.golden.report.txt | 13 ++++-- .../xmi/property_markers.golden.sysml | 7 ++++ .../migrate/testdata/xmi/property_markers.xmi | 12 ++++++ .../xmi/tool_profiles.golden.report.txt | 4 +- .../testdata/xmi/tool_profiles.golden.sysml | 4 +- tests/migrate/values_test.go | 2 +- 17 files changed, 103 insertions(+), 55 deletions(-) create mode 100644 changes/unreleased/untyped-property-kind.fixed.md diff --git a/changes/unreleased/untyped-property-kind.fixed.md b/changes/unreleased/untyped-property-kind.fixed.md new file mode 100644 index 0000000000..df5f5a2d0e --- /dev/null +++ b/changes/unreleased/untyped-property-kind.fixed.md @@ -0,0 +1 @@ +- **An untyped v1 property is no longer always a `ref`.** The SysML v1 migrator wrote every property without a type as `ref x;`, ignoring MagicDraw's «ValueProperty», «PartProperty», «SharedProperty», «ReferenceProperty» and «ConstraintProperty» markers and then writing the marker as an applied stereotype. An untyped property now takes the usage its marker names (`attribute`, `part`, `ref`, `constraint`), with the marker omitted as it is on typed properties; without a marker it is a `part` when its aggregation is composite and an `attribute` otherwise, reported approximated. A property whose default is an individual stays a `ref` typed by it. diff --git a/docs/project/sysml-v1-transformation-census-baseline.json b/docs/project/sysml-v1-transformation-census-baseline.json index cf6bde5602..cd7cfcd239 100644 --- a/docs/project/sysml-v1-transformation-census-baseline.json +++ b/docs/project/sysml-v1-transformation-census-baseline.json @@ -244,7 +244,7 @@ }, "uml:Property": { "pssm": 25, - "fixtures": 466 + "fixtures": 473 }, "uml:Property[qualifier]": { "pssm": 0, @@ -14639,14 +14639,16 @@ "filter" ], "ocl": "sha256:99f06b2a46c64e98963563afeee2a1b8cb76a43e5fae2ee1af153c247800e6fc", - "status": "faithful", + "status": "approximate", "implementation": [ - "internal/translate/migrate/migrate.go:featureKeyword" + "internal/translate/migrate/migrate.go:featureKeyword", + "internal/translate/migrate/migrate.go:untypedKeyword" ], "tests": [ - "tests/migrate/properties_common_test.go:TestUntypedPropertiesAreReferencesOutsideConstraintDefinitions" + "tests/migrate/properties_common_test.go:TestUntypedPropertiesAreAttributesInAndOutsideConstraintDefinitions", + "tests/migrate/relations_test.go:TestPropertyKindMarkersAreNotWritten" ], - "reason": "an untyped property is written as a reference usage; the scope counts every uml:Property, so it is an upper bound", + "reason": "an untyped property is written as the usage its tool's property-kind marker names, as a `ref` typed by its default individual, else as a `part` when composite and an `attribute` otherwise, rather than always as a reference usage; the scope counts every uml:Property, so it is an upper bound", "scope": [ "uml:Property" ] diff --git a/docs/project/sysml-v1-transformation-census.md b/docs/project/sysml-v1-transformation-census.md index 22a6f3c09b..9614f7a33c 100644 --- a/docs/project/sysml-v1-transformation-census.md +++ b/docs/project/sysml-v1-transformation-census.md @@ -34,7 +34,7 @@ that build one relationship of a target — share the verdict of the construct t -**Census:** 783 mapping classes — 505 ✅ faithful, 45 ⚠️ approximate, 42 ❌ not implemented, 191 ⛔ deliberate, 0 🚧 known failure, 0 ❔ unknown. +**Census:** 783 mapping classes — 504 ✅ faithful, 46 ⚠️ approximate, 42 ❌ not implemented, 191 ⛔ deliberate, 0 🚧 known failure, 0 ❔ unknown. | Package | Mappings | ✅ | ⚠️ | ❌ | ⛔ | 🚧 | ❔ | |---|---|---|---|---|---|---|---| @@ -56,7 +56,7 @@ that build one relationship of a target — share the verdict of the construct t | UML4SysML::Actions::StructuredActions | 3 | 2 | 1 | 0 | 0 | 0 | 0 | | UML4SysML::Actions::VariableActions | 31 | 26 | 0 | 0 | 5 | 0 | 0 | | UML4SysML::Activities | 68 | 60 | 0 | 0 | 8 | 0 | 0 | -| UML4SysML::Classification | 48 | 43 | 0 | 0 | 5 | 0 | 0 | +| UML4SysML::Classification | 48 | 42 | 1 | 0 | 5 | 0 | 0 | | UML4SysML::CommonBehavior | 57 | 28 | 29 | 0 | 0 | 0 | 0 | | UML4SysML::CommonStructure | 18 | 16 | 0 | 0 | 2 | 0 | 0 | | UML4SysML::InformationFlows | 9 | 8 | 0 | 0 | 1 | 0 | 0 | @@ -67,7 +67,7 @@ that build one relationship of a target — share the verdict of the construct t | UML4SysML::StructuredClassifiers | 39 | 15 | 0 | 0 | 24 | 0 | 0 | | UML4SysML::UseCases | 14 | 10 | 0 | 4 | 0 | 0 | 0 | | UML4SysML::Values | 33 | 14 | 0 | 3 | 16 | 0 | 0 | -| **Total** | 783 | 505 | 45 | 42 | 191 | 0 | 0 | +| **Total** | 783 | 504 | 46 | 42 | 191 | 0 | 0 | ## Census rows @@ -668,7 +668,7 @@ that build one relationship of a target — share the verdict of the construct t | `PropertyCommon_Mapping` (abstract) | `Property` | `Feature` | `internal/translate/migrate/migrate.go:feature`
`internal/translate/migrate/migrate.go:featureRedefinitions`
`internal/translate/migrate/migrate.go:featureDefault`
`internal/translate/migrate/migrate.go:featureTyping`
`internal/translate/migrate/migrate.go:multiplicity` | `tests/migrate/properties_common_test.go:TestPropertyCommonPartsAreWrittenOnTheUsage` | ✅ faithful | a property's type, subsetted and redefined properties, default value and multiplicity are written on its usage; the scope counts every uml:Property, so it is an upper bound | | `PropertySubsetting_Mapping` | `Property` | `Subsetting` | `internal/translate/migrate/translate.go:feature` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `PropertyTypedByClassInterface_Mapping` | `Property` | `OccurrenceUsage` | `internal/translate/migrate/migrate.go:featureKeyword`
`internal/translate/migrate/migrate.go:typeKeyword` | `internal/translate/migrate/occurrence_internal_test.go:TestPlainClassesAndTheirPropertiesUseOccurrenceForms`
`internal/translate/migrate/typefilter_internal_test.go:TestMetaclassFiltersExcludeNewConformingTypes` | ✅ faithful | a property typed by a plain class is written as an occurrence usage (`ref occurrence` unless composite); one typed by an interface as a port usage, the occurrence usage kind of the port def the interface becomes; the scope counts every uml:Property, so it is an upper bound | -| `PropertyUntyped_Mapping` | `NamedElement` | `ReferenceUsage` | `internal/translate/migrate/migrate.go:featureKeyword` | `tests/migrate/properties_common_test.go:TestUntypedPropertiesAreReferencesOutsideConstraintDefinitions` | ✅ faithful | an untyped property is written as a reference usage; the scope counts every uml:Property, so it is an upper bound | +| `PropertyUntyped_Mapping` | `NamedElement` | `ReferenceUsage` | `internal/translate/migrate/migrate.go:featureKeyword`
`internal/translate/migrate/migrate.go:untypedKeyword` | `tests/migrate/properties_common_test.go:TestUntypedPropertiesAreAttributesInAndOutsideConstraintDefinitions`
`tests/migrate/relations_test.go:TestPropertyKindMarkersAreNotWritten` | ⚠️ approximate | an untyped property is written as the usage its tool's property-kind marker names, as a `ref` typed by its default individual, else as a `part` when composite and an `attribute` otherwise, rather than always as a reference usage; the scope counts every uml:Property, so it is an upper bound | | `Realization_Mapping` | `Realization` | `Dependency` | `internal/translate/migrate/migrate.go:dependency`
`internal/translate/migrate/realization.go:realization` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `SlotFeatureTyping_Mapping` | `Slot` | `FeatureTyping` | `internal/translate/migrate/migrate.go:valueSlot`
`internal/translate/migrate/migrate.go:instanceSlot` | `tests/migrate/values_test.go:TestSlotContradictingItsFeatureIsUnmapped`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `SlotMembership_Mapping` | `Slot` | `FeatureMembership` | `internal/translate/migrate/migrate.go:valueSlot`
`internal/translate/migrate/migrate.go:instanceSlot` | `tests/migrate/values_test.go:TestSlotContradictingItsFeatureIsUnmapped`
`tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | @@ -1090,20 +1090,21 @@ SysML profile namespace. The counts are elements affected, not effort. | Rank | OMG mappings | Status | Scope | PSSM suite | Fixtures | Total | Reason | |---|---|---|---|---|---|---|---| -| 1 | 2: `CommonPseudostate_Mapping`, `PseudoState_Mapping` | ⚠️ approximate | `uml:Pseudostate` | 289 | 71 | 360 | choice and junction are written as state usages with StateMachines metadata, fork and join as nodes, history and terminate partially; a few pseudostates are unmapped (PSSM: 6, fixtures: 5) | -| 2 | 1: `Region_Mapping` | ⚠️ approximate | `uml:Region` | 248 | 54 | 302 | the region of a single-region state is flattened into its state rather than written as a state usage; orthogonal regions become the substates of a parallel state; empty regions are not written | -| 3 | 1: `OpaqueAction_Mapping` | ⚠️ approximate | `uml:OpaqueAction` | 0 | 158 | 158 | the body is translated into action statements when its language is read; otherwise the action is a stub keeping its pins (fixtures: 78 of 109 approximated) | -| 4 | 3: `OpaqueBehaviorMembership_Mapping`, `OpaqueBehaviorSpecification_Mapping`, `OpaqueBehavior_Mapping` | ⚠️ approximate | `uml:OpaqueBehavior` | 0 | 49 | 49 | an opaque behavior is written as an action def whose body is the translated statements, or as a calc def when it computes a result; a body that does not translate is kept as a comment (fixtures: 43 of 47 approximated) | -| 5 | 4: `ChangeEventReturnParameterMembership_Mapping`, `ChangeEventReturnParameter_Mapping`, `ChangeEvent_Mapping`, `TimeEvent_Mapping` | ⚠️ approximate | `uml:TimeEvent`
`uml:ChangeEvent`
`uml:CallEvent` | 8 | 17 | 25 | an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8) | -| 6 | 23: `TimeTriggerBindingConnector_Mapping`, `TimeTriggerCalculationUsage_Mapping`, `TimeTriggerEndFeatureMembership_Mapping`, `TimeTriggerEventChainingFeature_Mapping`, `TimeTriggerEventReturnParameterChainingFeature_Mapping`, `TimeTriggerExpressionFeatureTyping_Mapping`, `TimeTriggerExpressionFeatureValue_Mapping`, `TimeTriggerExpressionFeature_Mapping`, `TimeTriggerExpressionInvocationExpression_Mapping`, `TimeTriggerExpressionParameterMembership_Mapping`, `TimeTriggerFeatureMembership_Mapping`, `TimeTriggerFeatureTyping_Mapping`, `TimeTriggerFeatureValue_Mapping`, `TimeTriggerFeature_Mapping`, `TimeTriggerInvocationExpression_Mapping`, `TimeTriggerReferenceSubsetting_Mapping`, `TimeTriggerReferenceUsage2_Mapping`, `TimeTriggerReferenceUsage_Mapping`, `TimeTriggerReturnEndFeatureMembership_Mapping`, `TimeTriggerReturnParameterMembership_Mapping`, `TimeTriggerReturnParameter_Mapping`, `TimeTriggerReturnReferenceSubsetting_Mapping`, `TimeTriggerReturnReferenceUsage_Mapping` | ⚠️ approximate | `uml:TimeEvent` | 0 | 16 | 16 | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | -| 7 | 3: `EqualOperatorExpressionFeatureValue_Mapping`, `EqualOperatorExpressionFeature_Mapping`, `EqualOperatorExpressionOperandParameterMembership_Mapping` | ❌ not implemented | `uml:TestIdentityAction` | 14 | 0 | 14 | these are used only by TIAOperatorExpression_Mapping, as the private first/second operands of a TestIdentityAction's `==`, not by Expression trees; they are written with TestIdentityAction, which is not migrated (PSSM: 14) | -| 8 | 3: `TIAOperatorExpression_Mapping`, `TIAResultExpressionMembership_Mapping`, `TestIdentityAction_Mapping` | ❌ not implemented | `uml:TestIdentityAction` | 14 | 0 | 14 | a TestIdentityAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 9 | 9: `RICOAFeatureValueOperatorExpressionFeatureValue_Mapping`, `RICOAFeatureValueOperatorExpressionFeature_Mapping`, `RICOAFeatureValueOperatorExpression_Mapping`, `RICOAFeatureValueOperatorFeatureReferenceExpression_Mapping`, `RICOAFeatureValueOperatorMembership_Mapping`, `RICOAFeatureValueOperatorParameterMembership_Mapping`, `RICOAFeatureValue_Mapping`, `RICOAOutputPin_Mapping`, `ReadIsClassifiedObjectAction_Mapping` | ❌ not implemented | `uml:ReadIsClassifiedObjectAction` | 12 | 0 | 12 | a ReadIsClassifiedObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 10 | 1: `LoopNode_Mapping` | ⚠️ approximate | `uml:LoopNode` | 2 | 9 | 11 | a decider loop whose parts exchange values over pins the migrator writes is written as a v2 loop (the loops.xmi tested-first and post-tested fixtures): its loop variables and an `ended` flag as private features, its parts performing in the loop — test before body when tested first, else after — and its results read from the variables at the end; no corpus loop takes the loop form: PSSM's 2 fall back because their body outputs are structured-node output pins the migrator does not write, and TMT's 1 names no decider, so each keeps the body-once form and stays approximated per element; candidate erratum: the mapping gives only an ActionUsage and leaves its details undefined, so a loop's test and iteration are lost | -| 11 | 4: `UseCaseEmptySubjectReferenceUsage_Mapping`, `UseCaseObjectiveMembership_Mapping`, `UseCaseObjectiveRequirementUsage_Mapping`, `UseCaseObjectiveSubjectMembership_Mapping` | ❌ not implemented | `uml:UseCase` | 0 | 10 | 10 | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | -| 12 | 2: `TestCaseActivityReturnParameterMembership_Mapping`, `TestCaseActivity_Mapping` | ⚠️ approximate | `sysml:TestCase` | 0 | 5 | 5 | the test case is written as a `verification def` whose objective verifies the requirements, but its behavior (activity or interaction) is not migrated | -| 13 | 8: `ElementGroupMetadaMembership_Mapping`, `ElementGroupMetadataFeatureMembership_Mapping`, `ElementGroupMetadataFeatureTyping_Mapping`, `ElementGroupMetadataFeatureValue_Mapping`, `ElementGroupMetadataRedefinition_Mapping`, `ElementGroupMetadataReferenceUsage_Mapping`, `ElementGroupMetadataUsage_Mapping`, `ElementGroup_Mapping` | ❌ not implemented | `sysml:ElementGroup` | 0 | 0 | 0 | «ElementGroup» is not read by the migrator: the comment is written as a comment and no package grouping its members is written | -| 14 | 8: `ProblemRationaleMetadataFeatureMembership_Mapping`, `ProblemRationaleMetadataFeatureTyping_Mapping`, `ProblemRationaleMetadataFeatureValue_Mapping`, `ProblemRationaleMetadataMembership_Mapping`, `ProblemRationaleMetadataRedefinition_Mapping`, `ProblemRationaleMetadataReferenceUsage_Mapping`, `ProblemRationaleMetadataUsage_Mapping`, `ProblemRationale_Mapping` | ❌ not implemented | `sysml:Problem`
`sysml:Rationale` | 0 | 0 | 0 | «Problem» and «Rationale» are not read by the migrator: the comment is written as a comment without metadata saying which it is | -| 15 | 7: `REAFeatureValueOperatorExpressionFeatureTyping_Mapping`, `REAFeatureValueOperatorExpressionFeature_Mapping`, `REAFeatureValueOperatorExpressionMembership_Mapping`, `REAFeatureValueOperatorExpression_Mapping`, `REAFeatureValue_Mapping`, `REAOutputPin_Mapping`, `ReadExtentAction_Mapping` | ❌ not implemented | `uml:ReadExtentAction` | 0 | 0 | 0 | a ReadExtentAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 16 | 8: `TraceAnnotation_Mapping`, `TraceMetadataFeatureMembership_Mapping`, `TraceMetadataReferenceUsageFeatureValue_Mapping`, `TraceMetadataReferenceUsageRedefinition_Mapping`, `TraceMetadataReferenceUsage_Mapping`, `TraceMetadataUsageFeatureTyping_Mapping`, `TraceMetadataUsage_Mapping`, `Trace_Mapping` | ⚠️ approximate | `sysml:Trace` | 0 | 0 | 0 | a «Trace» is written as a plain dependency with a `/* «Trace» */` comment, not as a dependency annotated by trace metadata | +| 1 | 1: `PropertyUntyped_Mapping` | ⚠️ approximate | `uml:Property` | 25 | 473 | 498 | an untyped property is written as the usage its tool's property-kind marker names, as a `ref` typed by its default individual, else as a `part` when composite and an `attribute` otherwise, rather than always as a reference usage; the scope counts every uml:Property, so it is an upper bound | +| 2 | 2: `CommonPseudostate_Mapping`, `PseudoState_Mapping` | ⚠️ approximate | `uml:Pseudostate` | 289 | 71 | 360 | choice and junction are written as state usages with StateMachines metadata, fork and join as nodes, history and terminate partially; a few pseudostates are unmapped (PSSM: 6, fixtures: 5) | +| 3 | 1: `Region_Mapping` | ⚠️ approximate | `uml:Region` | 248 | 54 | 302 | the region of a single-region state is flattened into its state rather than written as a state usage; orthogonal regions become the substates of a parallel state; empty regions are not written | +| 4 | 1: `OpaqueAction_Mapping` | ⚠️ approximate | `uml:OpaqueAction` | 0 | 158 | 158 | the body is translated into action statements when its language is read; otherwise the action is a stub keeping its pins (fixtures: 78 of 109 approximated) | +| 5 | 3: `OpaqueBehaviorMembership_Mapping`, `OpaqueBehaviorSpecification_Mapping`, `OpaqueBehavior_Mapping` | ⚠️ approximate | `uml:OpaqueBehavior` | 0 | 49 | 49 | an opaque behavior is written as an action def whose body is the translated statements, or as a calc def when it computes a result; a body that does not translate is kept as a comment (fixtures: 43 of 47 approximated) | +| 6 | 4: `ChangeEventReturnParameterMembership_Mapping`, `ChangeEventReturnParameter_Mapping`, `ChangeEvent_Mapping`, `TimeEvent_Mapping` | ⚠️ approximate | `uml:TimeEvent`
`uml:ChangeEvent`
`uml:CallEvent` | 8 | 17 | 25 | an event is not written as an element of its own: the trigger that refers to it is written inline as an accept; an event no trigger refers to is skipped, and a CallEvent is unmapped (PSSM: 8) | +| 7 | 23: `TimeTriggerBindingConnector_Mapping`, `TimeTriggerCalculationUsage_Mapping`, `TimeTriggerEndFeatureMembership_Mapping`, `TimeTriggerEventChainingFeature_Mapping`, `TimeTriggerEventReturnParameterChainingFeature_Mapping`, `TimeTriggerExpressionFeatureTyping_Mapping`, `TimeTriggerExpressionFeatureValue_Mapping`, `TimeTriggerExpressionFeature_Mapping`, `TimeTriggerExpressionInvocationExpression_Mapping`, `TimeTriggerExpressionParameterMembership_Mapping`, `TimeTriggerFeatureMembership_Mapping`, `TimeTriggerFeatureTyping_Mapping`, `TimeTriggerFeatureValue_Mapping`, `TimeTriggerFeature_Mapping`, `TimeTriggerInvocationExpression_Mapping`, `TimeTriggerReferenceSubsetting_Mapping`, `TimeTriggerReferenceUsage2_Mapping`, `TimeTriggerReferenceUsage_Mapping`, `TimeTriggerReturnEndFeatureMembership_Mapping`, `TimeTriggerReturnParameterMembership_Mapping`, `TimeTriggerReturnParameter_Mapping`, `TimeTriggerReturnReferenceSubsetting_Mapping`, `TimeTriggerReturnReferenceUsage_Mapping` | ⚠️ approximate | `uml:TimeEvent` | 0 | 16 | 16 | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | +| 8 | 3: `EqualOperatorExpressionFeatureValue_Mapping`, `EqualOperatorExpressionFeature_Mapping`, `EqualOperatorExpressionOperandParameterMembership_Mapping` | ❌ not implemented | `uml:TestIdentityAction` | 14 | 0 | 14 | these are used only by TIAOperatorExpression_Mapping, as the private first/second operands of a TestIdentityAction's `==`, not by Expression trees; they are written with TestIdentityAction, which is not migrated (PSSM: 14) | +| 9 | 3: `TIAOperatorExpression_Mapping`, `TIAResultExpressionMembership_Mapping`, `TestIdentityAction_Mapping` | ❌ not implemented | `uml:TestIdentityAction` | 14 | 0 | 14 | a TestIdentityAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 10 | 9: `RICOAFeatureValueOperatorExpressionFeatureValue_Mapping`, `RICOAFeatureValueOperatorExpressionFeature_Mapping`, `RICOAFeatureValueOperatorExpression_Mapping`, `RICOAFeatureValueOperatorFeatureReferenceExpression_Mapping`, `RICOAFeatureValueOperatorMembership_Mapping`, `RICOAFeatureValueOperatorParameterMembership_Mapping`, `RICOAFeatureValue_Mapping`, `RICOAOutputPin_Mapping`, `ReadIsClassifiedObjectAction_Mapping` | ❌ not implemented | `uml:ReadIsClassifiedObjectAction` | 12 | 0 | 12 | a ReadIsClassifiedObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 11 | 1: `LoopNode_Mapping` | ⚠️ approximate | `uml:LoopNode` | 2 | 9 | 11 | a decider loop whose parts exchange values over pins the migrator writes is written as a v2 loop (the loops.xmi tested-first and post-tested fixtures): its loop variables and an `ended` flag as private features, its parts performing in the loop — test before body when tested first, else after — and its results read from the variables at the end; no corpus loop takes the loop form: PSSM's 2 fall back because their body outputs are structured-node output pins the migrator does not write, and TMT's 1 names no decider, so each keeps the body-once form and stays approximated per element; candidate erratum: the mapping gives only an ActionUsage and leaves its details undefined, so a loop's test and iteration are lost | +| 12 | 4: `UseCaseEmptySubjectReferenceUsage_Mapping`, `UseCaseObjectiveMembership_Mapping`, `UseCaseObjectiveRequirementUsage_Mapping`, `UseCaseObjectiveSubjectMembership_Mapping` | ❌ not implemented | `uml:UseCase` | 0 | 10 | 10 | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | +| 13 | 2: `TestCaseActivityReturnParameterMembership_Mapping`, `TestCaseActivity_Mapping` | ⚠️ approximate | `sysml:TestCase` | 0 | 5 | 5 | the test case is written as a `verification def` whose objective verifies the requirements, but its behavior (activity or interaction) is not migrated | +| 14 | 8: `ElementGroupMetadaMembership_Mapping`, `ElementGroupMetadataFeatureMembership_Mapping`, `ElementGroupMetadataFeatureTyping_Mapping`, `ElementGroupMetadataFeatureValue_Mapping`, `ElementGroupMetadataRedefinition_Mapping`, `ElementGroupMetadataReferenceUsage_Mapping`, `ElementGroupMetadataUsage_Mapping`, `ElementGroup_Mapping` | ❌ not implemented | `sysml:ElementGroup` | 0 | 0 | 0 | «ElementGroup» is not read by the migrator: the comment is written as a comment and no package grouping its members is written | +| 15 | 8: `ProblemRationaleMetadataFeatureMembership_Mapping`, `ProblemRationaleMetadataFeatureTyping_Mapping`, `ProblemRationaleMetadataFeatureValue_Mapping`, `ProblemRationaleMetadataMembership_Mapping`, `ProblemRationaleMetadataRedefinition_Mapping`, `ProblemRationaleMetadataReferenceUsage_Mapping`, `ProblemRationaleMetadataUsage_Mapping`, `ProblemRationale_Mapping` | ❌ not implemented | `sysml:Problem`
`sysml:Rationale` | 0 | 0 | 0 | «Problem» and «Rationale» are not read by the migrator: the comment is written as a comment without metadata saying which it is | +| 16 | 7: `REAFeatureValueOperatorExpressionFeatureTyping_Mapping`, `REAFeatureValueOperatorExpressionFeature_Mapping`, `REAFeatureValueOperatorExpressionMembership_Mapping`, `REAFeatureValueOperatorExpression_Mapping`, `REAFeatureValue_Mapping`, `REAOutputPin_Mapping`, `ReadExtentAction_Mapping` | ❌ not implemented | `uml:ReadExtentAction` | 0 | 0 | 0 | a ReadExtentAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 17 | 8: `TraceAnnotation_Mapping`, `TraceMetadataFeatureMembership_Mapping`, `TraceMetadataReferenceUsageFeatureValue_Mapping`, `TraceMetadataReferenceUsageRedefinition_Mapping`, `TraceMetadataReferenceUsage_Mapping`, `TraceMetadataUsageFeatureTyping_Mapping`, `TraceMetadataUsage_Mapping`, `Trace_Mapping` | ⚠️ approximate | `sysml:Trace` | 0 | 0 | 0 | a «Trace» is written as a plain dependency with a `/* «Trace» */` comment, not as a dependency annotated by trace metadata | diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 1ac8bd2cc1..6aefa04128 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -148,7 +148,9 @@ ties each of its mapping classes to the code that carries it out (or records why | InstanceSpecification naming no classifier, under a `SimulationConfig`'s `resultLocation`, whose slots are of features of one lineage of blocks ending in the configuration's target classifier or a general of it (a simulation tool's result snapshot) | the `individual part def` of the most special of those blocks, with its slots; the note says which owner classified it and for which configuration | mapped | | InstanceSpecification naming no classifier, anywhere else, or under a `resultLocation` with slots of features of blocks that are no one lineage or none the target is of | comment | **unmapped** — nothing classifies it; under a `resultLocation` the note says which owners its slots have and why they type no snapshot | | Slot contradicting its feature (more values than the multiplicity allows, a repeated value of a feature written unique — declared so in v1, or written without its `nonunique` by the rule above — a feature of a classifier the instance is not written to specialize, an instance that is not of the property's type or of its default individual, a value outside the document) | comment | **unmapped** | -| Slot of a port, or of an untyped property | comment (no individual can type a port; a `ref` without a type takes none) | **unmapped** | +| Slot of a port | comment (no individual can type a port) | **unmapped** | +| Property without a type | the usage MagicDraw's marker names — «ValueProperty» `attribute x;`, «PartProperty» `part x;`, «SharedProperty» or «ReferenceProperty» `ref x;`, «ConstraintProperty» `constraint x;` — the marker itself not written; one whose default is an individual, `ref x : 'the individual';` | mapped | +| Property without a type or marker | `part x;` when its aggregation is composite, `attribute x;` otherwise | approximated | | Property whose default is an InstanceSpecification of a block | the individual added to the usage's types, or its only type when the property is untyped; no `default` (a definition is not a v2 value). A port, a usage of another kind than the individual, or a usage whose type the individual is not an instance of, keeps its types and the default is a comment | approximated | | Literal default on a value type with no scalar base (a structured value type, an enumeration) | comment | approximated | | Real literal on an `Integer`/`Natural` feature, numeric string on a scalar feature | converted to the feature's scalar | mapped | diff --git a/internal/translate/migrate/behavior_members_internal_test.go b/internal/translate/migrate/behavior_members_internal_test.go index 486654d8b2..1d71131fac 100644 --- a/internal/translate/migrate/behavior_members_internal_test.go +++ b/internal/translate/migrate/behavior_members_internal_test.go @@ -71,14 +71,14 @@ func TestBehaviorMembersUnwritten(t *testing.T) { `, []string{"_q"}, []string{"_r", "_p"}, 1, - []string{"action def Run {\n view Rules {\n expose Run;\n expose p;\n render Views::asTextualNotation;\n }\n /* not migrated: Port 'q' — owned by a Activity, whose v2 body is its parameters and flow, not a place for a Port */\n ref p;\n assert constraint keep { true }\n}"}}, + []string{"action def Run {\n view Rules {\n expose Run;\n expose p;\n render Views::asTextualNotation;\n }\n /* not migrated: Port 'q' — owned by a Activity, whose v2 body is its parameters and flow, not a place for a Port */\n attribute p;\n assert constraint keep { true }\n}"}}, {"a state machine writes its constraint and attribute, and has no place for a port", `` + rule("_r", "keep") + ` `, []string{"_q"}, []string{"_r", "_p"}, 1, - []string{"state def Modes {\n view Rules {\n expose Modes;\n expose p;\n render Views::asTextualNotation;\n }\n /* not migrated: Port 'q' — owned by a StateMachine, whose v2 body is its parameters and states, not a place for a Port */\n assert constraint keep { true }\n ref p;"}}, + []string{"state def Modes {\n view Rules {\n expose Modes;\n expose p;\n render Views::asTextualNotation;\n }\n /* not migrated: Port 'q' — owned by a StateMachine, whose v2 body is its parameters and states, not a place for a Port */\n assert constraint keep { true }\n attribute p;"}}, } { t.Run(tc.name, func(t *testing.T) { shown := append(append([]string{"_b"}, tc.unwritten...), tc.written...) diff --git a/internal/translate/migrate/diagrams_internal_test.go b/internal/translate/migrate/diagrams_internal_test.go index b93cd7e526..e80bc740ab 100644 --- a/internal/translate/migrate/diagrams_internal_test.go +++ b/internal/translate/migrate/diagrams_internal_test.go @@ -108,7 +108,7 @@ func TestDiagramViews(t *testing.T) { []string{"view Pumps {\n expose Pump;\n expose Sys::Pump::rate;\n render Views::asTreeDiagram;\n }"}, Mapped, ""}, {"a diagram of a block is written in its part def", ``, diagram("_d", "Pump IBD", "_pump", "SysML Internal Block Diagram", "_rate"), - []string{"part def Pump {\n ref rate;\n view 'Pump IBD' {\n expose rate;\n render Views::asInterconnectionDiagram;\n }\n }"}, Mapped, ""}, + []string{"part def Pump {\n attribute rate;\n view 'Pump IBD' {\n expose rate;\n render Views::asInterconnectionDiagram;\n }\n }"}, Mapped, ""}, {"a diagram naming no owner is written where its extension is held", ``, diagram("_d", "Loose", "", "Class Diagram", "_pump"), []string{"view Loose {\n expose Sys::Pump;\n render Views::asTreeDiagram;\n}"}, Approximated, "the diagram names no owner; written at the top level"}, @@ -183,7 +183,7 @@ func TestDiagramViews(t *testing.T) { `, diagram("_d", "Valves", "_sys", "SysML Block Definition Diagram", "_status"), - []string{"action def Open {\n ref status;", "view Valves {\n expose Valve::Open::status;\n render Views::asTreeDiagram;\n }"}, Mapped, ""}, + []string{"action def Open {\n attribute status;", "view Valves {\n expose Valve::Open::status;\n render Views::asTreeDiagram;\n }"}, Mapped, ""}, {"a diagram of a method activity named like one of its members is numbered, the member keeping its name", ` @@ -195,7 +195,7 @@ func TestDiagramViews(t *testing.T) { `, diagram("_d1", "status", "_opening", "SysML Activity Diagram", "_status") + diagram("_d", "turn", "_opening", "SysML Activity Diagram", "_turn"), - []string{"action def Open {\n view 'status 2' {\n expose Valve::Open::status;\n render Views::asTextualNotation;", "view 'turn 2' : StandardViewDefinitions::ActionFlowView {\n expose Open;\n render Views::asInterconnectionDiagram;", "ref status;", "action turn {"}, Approximated, + []string{"action def Open {\n view 'status 2' {\n expose Valve::Open::status;\n render Views::asTextualNotation;", "view 'turn 2' : StandardViewDefinitions::ActionFlowView {\n expose Open;\n render Views::asInterconnectionDiagram;", "attribute status;", "action turn {"}, Approximated, "written as turn 2"}, {"a shown association end is exposed under the name its connection def declares", ` diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index 973f815c93..69bec62bf7 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -2254,11 +2254,15 @@ func (m *migration) featureKeyword(p *sysmlv1.Element, owner category) (keyword, } // untypedKeyword is the usage an untyped property is written as: the kind its -// tool's marker gives it, else a part when composite and an attribute otherwise. +// tool's marker gives it, a ref its default individual can type, else a part +// when composite and an attribute otherwise. func (m *migration) untypedKeyword(p *sysmlv1.Element) (keyword, prefix, note string) { if kind := m.markedPropertyKind(p); kind != "" { return kind, "", "" } + if m.defaultIndividual(p) != nil { + return "ref", "", "" + } if p.Attrs["aggregation"] == "composite" { return "part", "", "the untyped composite property is written as a part" } diff --git a/tests/migrate/properties_common_test.go b/tests/migrate/properties_common_test.go index e493e5b0fa..e37601ee36 100644 --- a/tests/migrate/properties_common_test.go +++ b/tests/migrate/properties_common_test.go @@ -29,7 +29,7 @@ func TestPropertyCommonPartsAreWrittenOnTheUsage(t *testing.T) { } } -func TestUntypedPropertiesAreReferencesOutsideConstraintDefinitions(t *testing.T) { +func TestUntypedPropertiesAreAttributesInAndOutsideConstraintDefinitions(t *testing.T) { r := migrateDocument(t, ` @@ -39,6 +39,6 @@ func TestUntypedPropertiesAreReferencesOutsideConstraintDefinitions(t *testing.T `, ``) wantClean(t, "property_untyped.sysml", r) - wantLine(t, r.Notation, "ref plain;") + wantLine(t, r.Notation, "attribute plain;") wantLine(t, r.Notation, "in attribute parameter[1];") } diff --git a/tests/migrate/relations_test.go b/tests/migrate/relations_test.go index 80507585e3..81333353fb 100644 --- a/tests/migrate/relations_test.go +++ b/tests/migrate/relations_test.go @@ -376,10 +376,10 @@ func TestUnwritableFeaturePartsAreDroppedWithNotes(t *testing.T) { `, ` `) wantLine(t, r.Notation, "ref intro;") - wantLine(t, r.Notation, "ref grid;") - wantLine(t, r.Notation, "ref many[0..*];") - wantLine(t, r.Notation, "ref margin {") - wantLine(t, r.Notation, `ref exposure default = "4";`) + wantLine(t, r.Notation, "attribute grid;") + wantLine(t, r.Notation, "attribute many[0..*];") + wantLine(t, r.Notation, "attribute margin {") + wantLine(t, r.Notation, `attribute exposure default = "4";`) if strings.Contains(string(r.Notation), "= NaN") { t.Errorf("a non-finite real was written:\n%s", r.Notation) } @@ -425,9 +425,9 @@ func TestClashingSiblingNamesAreDistinguished(t *testing.T) { `) - wantLine(t, r.Notation, "ref unnamed1;") - wantLine(t, r.Notation, "ref 'unnamed1 2';") - wantLine(t, r.Notation, "ref q :>> 'unnamed1 2';") + wantLine(t, r.Notation, "attribute unnamed1;") + wantLine(t, r.Notation, "attribute 'unnamed1 2';") + wantLine(t, r.Notation, "attribute q :>> 'unnamed1 2';") wantLine(t, r.Notation, "part def 'C 2'") wantLine(t, r.Notation, "part x : 'C 2';") for id, want := range map[string]string{ @@ -500,8 +500,8 @@ func TestExternalSpecializationsAreNotWritten(t *testing.T) { `, ` `) - wantLine(t, r.Notation, "ref a;") - wantLine(t, r.Notation, "ref b;") + wantLine(t, r.Notation, "attribute a;") + wantLine(t, r.Notation, "attribute b;") if strings.Contains(string(r.Notation), "_ext") || strings.Contains(string(r.Notation), "unnamed") { t.Errorf("notation refers to an external feature:\n%s", r.Notation) } @@ -1309,10 +1309,19 @@ func TestConstraintParametersStoredAsPortsAreInParameters(t *testing.T) { // MagicDraw's tagless property-kind markers say what the usage keyword says, // so they are not written; a same-named stereotype from any other profile, a -// marker carrying a tag, and one on a property of another kind are kept. +// marker carrying a tag, and one on a property of another kind are kept. An +// untyped property takes its marker's kind; without one it is an attribute, +// or a part when composite. func TestPropertyKindMarkersAreNotWritten(t *testing.T) { r := migrateFixtureFile(t, "property_markers") for _, line := range []string{ + "attribute load;", + "part trailer;", + "ref depot;", + "ref driver;", + "constraint rule;", + "attribute color;", + "part chassis;", "attribute mass : ScalarValues::Real;", "attribute mode : Mode;", "part engine : Engine;", @@ -1340,11 +1349,14 @@ func TestPropertyKindMarkersAreNotWritten(t *testing.T) { t.Errorf("«%s» written %d times, want %d", name, n, want) } } - for _, id := range []string{"_mass", "_mode", "_engine", "_lead", "_limit", "_ms_v", "_ms_wheel", "_ms_hub", "_ms_k", "_odd", "_axle", "_cabin", "_serial"} { + for _, id := range []string{"_mass", "_mode", "_engine", "_lead", "_limit", "_ms_v", "_ms_wheel", "_ms_hub", "_ms_k", "_odd", "_axle", "_cabin", "_serial", + "_load", "_trailer", "_depot", "_driver", "_rule"} { if es := entriesFor(r, id); len(es) != 1 || es[0].Verdict != migrate.Mapped { t.Errorf("%s entries = %+v", id, es) } } + wantNote(t, r, "_color", migrate.Approximated, "the untyped property is written as an attribute") + wantNote(t, r, "_chassis", migrate.Approximated, "the untyped composite property is written as a part") if es := entriesFor(r, "_spare"); len(es) != 1 || es[0].Verdict != migrate.Approximated || !strings.Contains(es[0].Note, "shared aggregation") { t.Errorf("_spare entries = %+v", es) } diff --git a/tests/migrate/testdata/xmi/collectors.golden.report.txt b/tests/migrate/testdata/xmi/collectors.golden.report.txt index 35ff1e3cbf..651e6819d5 100644 --- a/tests/migrate/testdata/xmi/collectors.golden.report.txt +++ b/tests/migrate/testdata/xmi/collectors.golden.report.txt @@ -17,7 +17,7 @@ Diagram Sketches::Sketch _diag_untyped -> Sketches::Sketch (a diagram of unknown kind written as a view rendered asTextualNotation; no diagram representation is serialized: what the diagram is and shows is unknown, and the view exposes nothing) Diagram Structure::Crane Structure _diag_structure -> Structure::'Crane Structure' (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; 2 of 7 shown elements are not written and not exposed; 3 of 7 shown ids resolve to no element) Property Structure::Crane::cable _prop_cable -> Structure::Crane::cable (shared aggregation is written as a reference part) -Property Structure::Crane::counterweight _prop_counterweight -> Structure::Crane::counterweight (the untyped property is written as a reference usage) +Property Structure::Crane::counterweight _prop_counterweight -> Structure::Crane::counterweight (the untyped composite property is written as a part) «Document» Class Yard Handbook::Yard Handbook _st_doc_handbook -> part def 'Yard Handbook'::'Yard Handbook Document' (the «Document» is written as a Document definition of 24 section(s); reads the 7 elements the tool lists as used on the SysML Block Definition Diagram 'Crane Structure', whose symbols are not serialized; the list need not be all it shows; leaves out 2 elements shown on the SysML Block Definition Diagram 'Crane Structure' that the archive does not describe; leaves out 1 element shown on the SysML Block Definition Diagram 'Crane Structure' that the migration does not write; leaves out 1 element shown on the SysML Block Definition Diagram 'Crane Structure' written within the elements owning them, with no v2 element of their own; «FilterByNames» Yard Viewpoints::Optional Drafts Viewpoint::Optional Drafts Method::Filter By Names names the elements it keeps, since a WhereName query matches v2 names: the «Block» Class Drafts:: is named unnamed in v2) Activity Yard Viewpoints::All Diagrams Viewpoint::All Diagrams Method _act_all -> 'Yard Viewpoints'::'All Diagrams Viewpoint'::'All Diagrams Method' (the classifier behavior is run by every object of Yard Viewpoints::All Diagrams Viewpoint as its usage all Diagrams Method) «FilterByDiagramType» CallBehaviorAction Yard Viewpoints::All Diagrams Viewpoint::All Diagrams Method::Filter By Diagram Type _all_filter -> 'Filter By Diagram Type' (a step with no behavior and no duration; it passes the token on) diff --git a/tests/migrate/testdata/xmi/collectors.golden.sysml b/tests/migrate/testdata/xmi/collectors.golden.sysml index 90cc5d59c7..6f99a32149 100644 --- a/tests/migrate/testdata/xmi/collectors.golden.sysml +++ b/tests/migrate/testdata/xmi/collectors.golden.sysml @@ -4,7 +4,7 @@ package Structure { part hook : Hook; ref part cable : Cable; ref part operator : Operator; - ref counterweight; + part counterweight; connect hook to cable; view 'Crane Internals' { expose hook; diff --git a/tests/migrate/testdata/xmi/property_markers.golden.report.txt b/tests/migrate/testdata/xmi/property_markers.golden.report.txt index 48c0e05a46..a42bf03af7 100644 --- a/tests/migrate/testdata/xmi/property_markers.golden.report.txt +++ b/tests/migrate/testdata/xmi/property_markers.golden.report.txt @@ -1,12 +1,14 @@ # SysML v1 to v2 migration report: property_markers.xmi # exported by Example UML Tool -# migrated 26 element(s): 24 mapped, 2 approximated, 0 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 33 element(s): 29 mapped, 4 approximated, 0 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (2) +## approximated (4) Constraint Fleet::MaxSpeed:: _ms_rule -> Fleet::MaxSpeed (opaque expression copied verbatim (language SysML)) +Property Fleet::Vehicle::chassis _chassis -> Fleet::Vehicle::chassis (the untyped composite property is written as a part) +Property Fleet::Vehicle::color _color -> Fleet::Vehicle::color (the untyped property is written as an attribute) «SharedProperty» Property Fleet::Vehicle::spare _spare -> Fleet::Vehicle::spare (shared aggregation is written as a reference part) -## mapped (24) +## mapped (29) Package Fleet _sys -> Fleet «Block» Class Fleet::Engine _engine_def -> Fleet::Engine «ConstraintBlock» Class Fleet::MaxSpeed _maxspeed -> Fleet::MaxSpeed @@ -20,13 +22,18 @@ EnumerationLiteral Fleet::Mode::sport _modes_sport -> Fleet::Mode::sport «Block» Class Fleet::Vehicle _vehicle -> Fleet::Vehicle «PartProperty» Property Fleet::Vehicle::axle _axle -> Fleet::Vehicle::axle «PartProperty» Property Fleet::Vehicle::cabin _cabin -> Fleet::Vehicle::cabin +«SharedProperty» Property Fleet::Vehicle::depot _depot -> Fleet::Vehicle::depot +«ReferenceProperty» Property Fleet::Vehicle::driver _driver -> Fleet::Vehicle::driver «PartProperty» Property Fleet::Vehicle::engine _engine -> Fleet::Vehicle::engine «ReferenceProperty» Property Fleet::Vehicle::lead _lead -> Fleet::Vehicle::lead «ConstraintProperty» Property Fleet::Vehicle::limit _limit -> Fleet::Vehicle::limit +«ValueProperty» Property Fleet::Vehicle::load _load -> Fleet::Vehicle::load «ValueProperty» Property Fleet::Vehicle::mass _mass -> Fleet::Vehicle::mass «ValueProperty» Property Fleet::Vehicle::mode _mode -> Fleet::Vehicle::mode «ValueProperty» Property Fleet::Vehicle::odd _odd -> Fleet::Vehicle::odd +«ConstraintProperty» Property Fleet::Vehicle::rule _rule -> Fleet::Vehicle::rule «ValueProperty» Property Fleet::Vehicle::serial _serial -> Fleet::Vehicle::serial («ValueProperty» from http://example.com/legacy/customization is applied from a profile the document does not define; the application is kept as a comment) +«PartProperty» Property Fleet::Vehicle::trailer _trailer -> Fleet::Vehicle::trailer «Block» Class Fleet::Wheel _wheel -> Fleet::Wheel Model Model _m (the root model's members are written at the top level) Profile Vehicle Profile _vp -> 'Vehicle Profile' diff --git a/tests/migrate/testdata/xmi/property_markers.golden.sysml b/tests/migrate/testdata/xmi/property_markers.golden.sysml index 08deb2f184..feb122da83 100644 --- a/tests/migrate/testdata/xmi/property_markers.golden.sysml +++ b/tests/migrate/testdata/xmi/property_markers.golden.sysml @@ -18,6 +18,13 @@ package Fleet { part cabin : Engine { @'Vehicle Profile'::PartProperty; } + attribute load; + part trailer; + ref depot; + ref driver; + constraint rule; + attribute color; + part chassis; attribute serial : ScalarValues::String { /* applied stereotype «ValueProperty» */ } diff --git a/tests/migrate/testdata/xmi/property_markers.xmi b/tests/migrate/testdata/xmi/property_markers.xmi index f9ce60a928..154b0a5149 100644 --- a/tests/migrate/testdata/xmi/property_markers.xmi +++ b/tests/migrate/testdata/xmi/property_markers.xmi @@ -30,6 +30,13 @@ + + + + + + + @@ -74,6 +81,11 @@ + + + + + diff --git a/tests/migrate/testdata/xmi/tool_profiles.golden.report.txt b/tests/migrate/testdata/xmi/tool_profiles.golden.report.txt index e393dd6084..1f964b1c7b 100644 --- a/tests/migrate/testdata/xmi/tool_profiles.golden.report.txt +++ b/tests/migrate/testdata/xmi/tool_profiles.golden.report.txt @@ -4,9 +4,9 @@ ## approximated (4) StateMachine Mockups::Clock::Ticking _ui_clock_sm -> Mockups::Clock::Ticking (the classifier behavior is run by every object of Mockups::Clock as its usage ticking) -Property Mockups::Clock::ticks _ui_clock_ticks -> Mockups::Clock::ticks (the untyped property is written as a reference usage) +Property Mockups::Clock::ticks _ui_clock_ticks -> Mockups::Clock::ticks (the untyped property is written as an attribute) «SimulationConfig» Class Runs::Run pump _cfg -> Runs::'Run pump' (neither System::Pump nor any general of it has a classifier behavior, so the configuration only holds 'Pump') -Property System::Pump::mass _pump_mass -> System::Pump::mass (the untyped property is written as a reference usage) +Property System::Pump::mass _pump_mass -> System::Pump::mass (the untyped property is written as an attribute) ## mapped (15) Package Customizations _cust -> Customizations diff --git a/tests/migrate/testdata/xmi/tool_profiles.golden.sysml b/tests/migrate/testdata/xmi/tool_profiles.golden.sysml index d34f07ae5c..ff9b5b512b 100644 --- a/tests/migrate/testdata/xmi/tool_profiles.golden.sysml +++ b/tests/migrate/testdata/xmi/tool_profiles.golden.sysml @@ -5,7 +5,7 @@ package 'My Profile' { } package System { part def Pump { - ref mass; + attribute mass; @'My Profile'::Tracked { ticket = "T-42"; } @@ -14,7 +14,7 @@ package System { package Customizations; package Mockups { occurrence def Clock { - ref ticks; + attribute ticks; state def Ticking { entry; then Running; state Running; diff --git a/tests/migrate/values_test.go b/tests/migrate/values_test.go index e78ea35ba7..0746318727 100644 --- a/tests/migrate/values_test.go +++ b/tests/migrate/values_test.go @@ -792,7 +792,7 @@ func TestPartSlotWithoutAConformingIndividualIsUnmapped(t *testing.T) { wantNote(t, r, "_sl3", migrate.Unmapped, "the slot's value 'other 1' is not an instance of MCS, the type of mcs") wantNote(t, r, "_sl4", migrate.Unmapped, "the slot of port bus is not written: v2 has no individual port for it to be typed by") wantNote(t, r, "_sl5", migrate.Unmapped, "the slot of port link is not written: v2 has no individual port for it to be typed by") - wantNote(t, r, "_sl6", migrate.Unmapped, "the slot of loose is not written: the property is written as a plain ref, which cannot be typed by an individual") + wantNote(t, r, "_sl6", migrate.Unmapped, "the individual mcs 1 is a definition, which is not a v2 value") wantNote(t, r, "_sl7", migrate.Unmapped, "the slot's value 'other 1' is an individual part def, which cannot type an item") wantClean(t, "bad-part-slots.sysml", r) } From 37c9337fea7608e2d5af27dd3f47220dd92b3791 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:55:18 +0000 Subject: [PATCH 3/5] fix(migrate): a slot of an untyped part is typed by its individual instead of crashing Co-Authored-By: jason.han --- .../unreleased/untyped-property-kind.fixed.md | 2 +- internal/translate/migrate/migrate.go | 3 +- tests/migrate/values_test.go | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/changes/unreleased/untyped-property-kind.fixed.md b/changes/unreleased/untyped-property-kind.fixed.md index df5f5a2d0e..b8af4ec279 100644 --- a/changes/unreleased/untyped-property-kind.fixed.md +++ b/changes/unreleased/untyped-property-kind.fixed.md @@ -1 +1 @@ -- **An untyped v1 property is no longer always a `ref`.** The SysML v1 migrator wrote every property without a type as `ref x;`, ignoring MagicDraw's «ValueProperty», «PartProperty», «SharedProperty», «ReferenceProperty» and «ConstraintProperty» markers and then writing the marker as an applied stereotype. An untyped property now takes the usage its marker names (`attribute`, `part`, `ref`, `constraint`), with the marker omitted as it is on typed properties; without a marker it is a `part` when its aggregation is composite and an `attribute` otherwise, reported approximated. A property whose default is an individual stays a `ref` typed by it. +- **An untyped v1 property is no longer always a `ref`.** The SysML v1 migrator wrote every property without a type as `ref x;`, ignoring MagicDraw's «ValueProperty», «PartProperty», «SharedProperty», «ReferenceProperty» and «ConstraintProperty» markers and then writing the marker as an applied stereotype. An untyped property now takes the usage its marker names (`attribute`, `part`, `ref`, `constraint`), with the marker omitted as it is on typed properties; without a marker it is a `part` when its aggregation is composite and an `attribute` otherwise, reported approximated. A property whose default is an individual stays a `ref` typed by it. A slot of such an untyped `part` holding an individual, which crashed the migration, is written as `individual part :>> x : 'the individual';`. diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index 69bec62bf7..b69f9b5229 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -1882,7 +1882,8 @@ func (m *migration) instanceSlot(e, slot, f *sysmlv1.Element, kw, prefix string) if kind == catNone || !individualTypes(kind, kw) { return nil, slotValueSubject + describe(inst) + " is an " + individualKeyword(kind) + ", which cannot type " + article(kw) + kw, false } - if !m.instanceOf(classifiers, t) { + // An untyped property is typed by any individual of its kind. + if t != nil && !m.instanceOf(classifiers, t) { return nil, slotValueSubject + describe(inst) + " is not an instance of " + qualifiedName(t) + ", the type of " + f.Name, false } // The default individual types the property, so a slot can only repeat it. diff --git a/tests/migrate/values_test.go b/tests/migrate/values_test.go index 0746318727..8b64ee5398 100644 --- a/tests/migrate/values_test.go +++ b/tests/migrate/values_test.go @@ -797,6 +797,36 @@ func TestPartSlotWithoutAConformingIndividualIsUnmapped(t *testing.T) { wantClean(t, "bad-part-slots.sysml", r) } +// A slot of an untyped part — composite, or marked «PartProperty» — holds any +// individual part: the property has no type to check the instance against. +func TestUntypedPartSlotIsTypedByItsIndividual(t *testing.T) { + r := migrateDocument(t, ` + + + + + + + + + + + + + + `, ` + + + `) + wantLine(t, r.Notation, "part wheel;") + wantLine(t, r.Notation, "part spare;") + wantLine(t, r.Notation, "individual part :>> wheel : 'wheel 1';") + wantLine(t, r.Notation, "individual part :>> spare : 'wheel 1';") + wantNote(t, r, "_sw", migrate.Mapped, "") + wantNote(t, r, "_ss", migrate.Mapped, "") + wantClean(t, "untyped-part-slots.sysml", r) +} + // An individual takes the kind of its classifier — `individual constraint // def` for an instance of a constraint block — and its slots redefine the // parameters with their `in` direction. Classifiers of another kind are not From 1888dee78894de06a0d96fb2f369c2e59bc879d1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:05:24 +0000 Subject: [PATCH 4/5] fix(migrate): a slot of an untyped part is checked against the type the part inherits by redefinition, subsetting or name Co-Authored-By: jason.han --- .../unreleased/untyped-property-kind.fixed.md | 2 +- internal/translate/migrate/migrate.go | 34 ++++++++++++++++- tests/migrate/values_test.go | 38 +++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/changes/unreleased/untyped-property-kind.fixed.md b/changes/unreleased/untyped-property-kind.fixed.md index b8af4ec279..7022ec0069 100644 --- a/changes/unreleased/untyped-property-kind.fixed.md +++ b/changes/unreleased/untyped-property-kind.fixed.md @@ -1 +1 @@ -- **An untyped v1 property is no longer always a `ref`.** The SysML v1 migrator wrote every property without a type as `ref x;`, ignoring MagicDraw's «ValueProperty», «PartProperty», «SharedProperty», «ReferenceProperty» and «ConstraintProperty» markers and then writing the marker as an applied stereotype. An untyped property now takes the usage its marker names (`attribute`, `part`, `ref`, `constraint`), with the marker omitted as it is on typed properties; without a marker it is a `part` when its aggregation is composite and an `attribute` otherwise, reported approximated. A property whose default is an individual stays a `ref` typed by it. A slot of such an untyped `part` holding an individual, which crashed the migration, is written as `individual part :>> x : 'the individual';`. +- **An untyped v1 property is no longer always a `ref`.** The SysML v1 migrator wrote every property without a type as `ref x;`, ignoring MagicDraw's «ValueProperty», «PartProperty», «SharedProperty», «ReferenceProperty» and «ConstraintProperty» markers and then writing the marker as an applied stereotype. An untyped property now takes the usage its marker names (`attribute`, `part`, `ref`, `constraint`), with the marker omitted as it is on typed properties; without a marker it is a `part` when its aggregation is composite and an `attribute` otherwise, reported approximated. A property whose default is an individual stays a `ref` typed by it. A slot of such an untyped `part` holding an individual, which crashed the migration, is written as `individual part :>> x : 'the individual';`, checked against the type the part inherits when it redefines, subsets or shadows a typed one. diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index b69f9b5229..a78ac6a1dc 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -1860,7 +1860,7 @@ func (m *migration) valueSlot(e, slot, f *sysmlv1.Element, dir string) ([]string // instanceSlot resolves a slot holding instances: one redefines the feature // typed by its individual; several each subset it under a redefinition counting them. func (m *migration) instanceSlot(e, slot, f *sysmlv1.Element, kw, prefix string) ([]string, string, bool) { - t := m.model.Ref(f, "type") + t := m.effectiveType(f) var refs []string for _, v := range slot.Owned("value") { if v.Type != "InstanceValue" { @@ -1882,7 +1882,7 @@ func (m *migration) instanceSlot(e, slot, f *sysmlv1.Element, kw, prefix string) if kind == catNone || !individualTypes(kind, kw) { return nil, slotValueSubject + describe(inst) + " is an " + individualKeyword(kind) + ", which cannot type " + article(kw) + kw, false } - // An untyped property is typed by any individual of its kind. + // A property with no type, its own or inherited, takes any individual of its kind. if t != nil && !m.instanceOf(classifiers, t) { return nil, slotValueSubject + describe(inst) + " is not an instance of " + qualifiedName(t) + ", the type of " + f.Name, false } @@ -1918,6 +1918,36 @@ func (m *migration) instanceSlot(e, slot, f *sysmlv1.Element, kw, prefix string) return lines, "", true } +// effectiveType is the type a property's usage has: its own, else the one it +// inherits from the written property it redefines, subsets or shadows by name. +func (m *migration) effectiveType(f *sysmlv1.Element) *sysmlv1.Element { + seen := map[*sysmlv1.Element]bool{} + var walk func(*sysmlv1.Element) *sysmlv1.Element + walk = func(p *sysmlv1.Element) *sysmlv1.Element { + if p == nil || seen[p] { + return nil + } + seen[p] = true + if t := m.model.Ref(p, "type"); t != nil { + return t + } + for _, role := range []string{"redefinedProperty", "subsettedProperty"} { + for _, r := range m.model.Refs(p, role) { + if m.written(r) { + if t := walk(r); t != nil { + return t + } + } + } + } + if r, redefinable := m.shadowed(p); redefinable { + return walk(r) + } + return nil + } + return walk(f) +} + // article is the indefinite article before a word: "an item", "a part". func article(word string) string { if strings.ContainsRune("aeiou", rune(word[0])) { diff --git a/tests/migrate/values_test.go b/tests/migrate/values_test.go index 8b64ee5398..a2f0cb6058 100644 --- a/tests/migrate/values_test.go +++ b/tests/migrate/values_test.go @@ -827,6 +827,44 @@ func TestUntypedPartSlotIsTypedByItsIndividual(t *testing.T) { wantClean(t, "untyped-part-slots.sysml", r) } +// An untyped part that redefines a typed one, by declaration or by sharing its +// name, inherits the type: a slot holding an individual of another type is unmapped. +func TestUntypedRedefiningPartSlotKeepsTheInheritedType(t *testing.T) { + r := migrateDocument(t, ` + + + + + + + + + + + + + + + + + + + + + `, ` + + + + `) + wantLine(t, r.Notation, "part wheel :>> wheel;") + wantLine(t, r.Notation, "part front :>> spare;") + wantNoLine(t, r.Notation, "individual part :>> wheel : 'engine 1';") + wantNote(t, r, "_sw", migrate.Unmapped, "the slot's value 'engine 1' is not an instance of Wheel, the type of wheel") + wantLine(t, r.Notation, "individual part :>> front : 'wheel 1';") + wantNote(t, r, "_ss", migrate.Mapped, "") + wantClean(t, "redefining-part-slots.sysml", r) +} + // An individual takes the kind of its classifier — `individual constraint // def` for an instance of a constraint block — and its slots redefine the // parameters with their `in` direction. Classifiers of another kind are not From 404a6af62bf9309acb925cd690730c04913ba1e7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:26:27 +0000 Subject: [PATCH 5/5] fix(migrate): a slot of an untyped part is checked against every type the part inherits by redefinition, subsetting or name Co-Authored-By: jason.han --- .../unreleased/untyped-property-kind.fixed.md | 2 +- internal/translate/migrate/migrate.go | 37 +++++++++++-------- tests/migrate/values_test.go | 35 ++++++++++++++++++ 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/changes/unreleased/untyped-property-kind.fixed.md b/changes/unreleased/untyped-property-kind.fixed.md index 7022ec0069..0c70f9a3ea 100644 --- a/changes/unreleased/untyped-property-kind.fixed.md +++ b/changes/unreleased/untyped-property-kind.fixed.md @@ -1 +1 @@ -- **An untyped v1 property is no longer always a `ref`.** The SysML v1 migrator wrote every property without a type as `ref x;`, ignoring MagicDraw's «ValueProperty», «PartProperty», «SharedProperty», «ReferenceProperty» and «ConstraintProperty» markers and then writing the marker as an applied stereotype. An untyped property now takes the usage its marker names (`attribute`, `part`, `ref`, `constraint`), with the marker omitted as it is on typed properties; without a marker it is a `part` when its aggregation is composite and an `attribute` otherwise, reported approximated. A property whose default is an individual stays a `ref` typed by it. A slot of such an untyped `part` holding an individual, which crashed the migration, is written as `individual part :>> x : 'the individual';`, checked against the type the part inherits when it redefines, subsets or shadows a typed one. +- **An untyped v1 property is no longer always a `ref`.** The SysML v1 migrator wrote every property without a type as `ref x;`, ignoring MagicDraw's «ValueProperty», «PartProperty», «SharedProperty», «ReferenceProperty» and «ConstraintProperty» markers and then writing the marker as an applied stereotype. An untyped property now takes the usage its marker names (`attribute`, `part`, `ref`, `constraint`), with the marker omitted as it is on typed properties; without a marker it is a `part` when its aggregation is composite and an `attribute` otherwise, reported approximated. A property whose default is an individual stays a `ref` typed by it. A slot of such an untyped `part` holding an individual, which crashed the migration, is written as `individual part :>> x : 'the individual';`, checked against every type the part inherits when it redefines, subsets or shadows typed ones. diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index a78ac6a1dc..c3e1d380ae 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -1860,7 +1860,7 @@ func (m *migration) valueSlot(e, slot, f *sysmlv1.Element, dir string) ([]string // instanceSlot resolves a slot holding instances: one redefines the feature // typed by its individual; several each subset it under a redefinition counting them. func (m *migration) instanceSlot(e, slot, f *sysmlv1.Element, kw, prefix string) ([]string, string, bool) { - t := m.effectiveType(f) + types := m.effectiveTypes(f) var refs []string for _, v := range slot.Owned("value") { if v.Type != "InstanceValue" { @@ -1883,8 +1883,10 @@ func (m *migration) instanceSlot(e, slot, f *sysmlv1.Element, kw, prefix string) return nil, slotValueSubject + describe(inst) + " is an " + individualKeyword(kind) + ", which cannot type " + article(kw) + kw, false } // A property with no type, its own or inherited, takes any individual of its kind. - if t != nil && !m.instanceOf(classifiers, t) { - return nil, slotValueSubject + describe(inst) + " is not an instance of " + qualifiedName(t) + ", the type of " + f.Name, false + for _, t := range types { + if !m.instanceOf(classifiers, t) { + return nil, slotValueSubject + describe(inst) + " is not an instance of " + qualifiedName(t) + ", the type of " + f.Name, false + } } // The default individual types the property, so a slot can only repeat it. if d, _ := m.typingIndividual(f, kw); d != nil && d != inst { @@ -1918,34 +1920,37 @@ func (m *migration) instanceSlot(e, slot, f *sysmlv1.Element, kw, prefix string) return lines, "", true } -// effectiveType is the type a property's usage has: its own, else the one it -// inherits from the written property it redefines, subsets or shadows by name. -func (m *migration) effectiveType(f *sysmlv1.Element) *sysmlv1.Element { +// effectiveTypes are the types a property's usage must conform to: its own, +// else those of every written property it redefines, subsets or shadows by name. +func (m *migration) effectiveTypes(f *sysmlv1.Element) []*sysmlv1.Element { + var types []*sysmlv1.Element seen := map[*sysmlv1.Element]bool{} - var walk func(*sysmlv1.Element) *sysmlv1.Element - walk = func(p *sysmlv1.Element) *sysmlv1.Element { + var walk func(*sysmlv1.Element) + walk = func(p *sysmlv1.Element) { if p == nil || seen[p] { - return nil + return } seen[p] = true if t := m.model.Ref(p, "type"); t != nil { - return t + if !seen[t] { + seen[t] = true + types = append(types, t) + } + return } for _, role := range []string{"redefinedProperty", "subsettedProperty"} { for _, r := range m.model.Refs(p, role) { if m.written(r) { - if t := walk(r); t != nil { - return t - } + walk(r) } } } if r, redefinable := m.shadowed(p); redefinable { - return walk(r) + walk(r) } - return nil } - return walk(f) + walk(f) + return types } // article is the indefinite article before a word: "an item", "a part". diff --git a/tests/migrate/values_test.go b/tests/migrate/values_test.go index a2f0cb6058..4c5cac2cfb 100644 --- a/tests/migrate/values_test.go +++ b/tests/migrate/values_test.go @@ -865,6 +865,41 @@ func TestUntypedRedefiningPartSlotKeepsTheInheritedType(t *testing.T) { wantClean(t, "redefining-part-slots.sysml", r) } +// An untyped part subsetting several typed ones must hold an instance of every +// type: an individual of only the first is unmapped, one of all is written. +func TestUntypedPartSlotKeepsEveryInheritedType(t *testing.T) { + r := migrateDocument(t, ` + + + + + + + + + + + + + + + + + + + + `, ` + + + `) + wantLine(t, r.Notation, "part spare :> vehicle :> wheel;") + wantNoLine(t, r.Notation, "individual part :>> spare : 'vehicle 1';") + wantNote(t, r, "_ss", migrate.Unmapped, "the slot's value 'vehicle 1' is not an instance of Wheel, the type of spare") + wantLine(t, r.Notation, "individual part :>> trailer : 'wheel 1';") + wantNote(t, r, "_st", migrate.Mapped, "") + wantClean(t, "subsetting-part-slots.sysml", r) +} + // An individual takes the kind of its classifier — `individual constraint // def` for an instance of a constraint block — and its slots redefine the // parameters with their `in` direction. Classifiers of another kind are not